실행환경
전자정부 표준프레임워크 실행환경은 ‘전자정부 서비스의 품질향상 및 정보화 투자 효율성 향상’을 위해 개발프레임워크 실행환경 표준을 정립하고, 개발프레임워크 표준 적용을 통한 응용 SW의 표준화 및 품질과 재사용성 향상을 목표로 한다. 또한, 모바일 웹의 사용성과 편의성 증대를 위하여 기존 실행환경 기반 개발이 가능한 모바일 웹 기반의 표준패턴 및 가이드 코드를 제공한다.
이 섹션의 다중 페이지 출력 화면임. 여기를 클릭하여 프린트.
전자정부 표준프레임워크 실행환경은 ‘전자정부 서비스의 품질향상 및 정보화 투자 효율성 향상’을 위해 개발프레임워크 실행환경 표준을 정립하고, 개발프레임워크 표준 적용을 통한 응용 SW의 표준화 및 품질과 재사용성 향상을 목표로 한다. 또한, 모바일 웹의 사용성과 편의성 증대를 위하여 기존 실행환경 기반 개발이 가능한 모바일 웹 기반의 표준패턴 및 가이드 코드를 제공한다.
표준프레임워크 실행환경은 응용SW의 구성기반으로서, 응용SW 실행 시 필요한 기본 기능을 제공하는 환경을 의미한다.
즉, 전자정부 업무 구현을 목적으로 개발된 프로그램이 사용자가 의도하는 대로 정상적으로 실행될 수 있도록 지원하는 재사용 가능한 서버 실행 모듈, SW구조의 집합을 의미 한다.

전자정부 표준프레임워크 실행환경은 ‘전자정부 서비스의 품질향상 및 정보화 투자 효율성 향상’을 위해 개발프레임워크 실행환경 표준을 정립하고, 개발프레임워크 표준 적용을 통한 응용 SW의 표준화 및 품질과 재사용성 향상을 목표로 한다.
또한, 모바일 웹의 사용성과 편의성 증대를 위하여 기존 실행환경 기반 개발이 가능한 모바일 웹 기반의 표준패턴 및 가이드 코드를 제공한다.
현재 전자정부는 유사한 기능을 가지는 다양한 종류 및 버전의 프레임워크를 개별 시스템 단위로 적용/관리하고 있으며, 이에 따라 다양한 문제점들이 발생하고 있다.
전자정부에 적용된 개발 프레임워크는 Black Box 형태로 제공되어 사업자의 기술지원 없이는 응용 SW를 유지보수하기 어렵기 때문에 사업자에 대한 의존성이 발생한다.
복수개의 개발프레임워크가 적용된 사업의 경우, 개발프레임워크에 따라 개발표준 정의, 개발자 수급, 교육시행 등 별도의 유지보수 체계를 갖추는 중복 투자가 발생하며, 개발프레임워크의 체계적인 관리절차의 미비로 동일 개발프레임워크라 하더라도 버전 관리에 어려움이 있다.
따라서 전자정부 개발프레임워크의 표준화를 통하여
표준프레임워크 실행환경은 전자정부 프레임워크에 대한 요구사항에 기반하고 기존 전자정부 프로젝트에 적용된 업체별 프레임워크를 비교 분석하여 38개 실행환경 프레임워크 서비스를 정의하였다.
표준프레임워크 실행환경은 서비스별 오픈 소스 소프트웨어 심사 과정을 거쳐 선정한 경량화된 표준프레임워크로서 사실상 업계 표준에 가까운 Spring 프레임워크를 기반으로 한다.
연계 솔루션에 독립된 전자정부 표준프레임워크 표준 연계 인터페이스를 정의하고 웹서비스 기반 구현체를 제공하며 모바일 웹의 경우 UX처리를 통합 웹서비스를 구현한다.
전자정부 표준프레임워크 실행환경은 8개 서비스 그룹으로 구성되며 38개 서비스를 제공한다. 실행환경 서비스 그룹 및 서비스는 아래 그림과 같다.

화면처리 서비스그룹은 업무처리 서비스와 사용자간의 인터페이스를 담당하는 서비스로 사용자 화면 구성 및 사용자 입력 정보 검증 등의 기능을 지원한다.
UX 처리 서비스는 모바일 웹의 사용성과 편의성 증대를 위하여 사용자 경험 기능을 제공하고 시각,인터페이스,효과 경험이 가능하도록 지원한다.
업무처리 서비스는 업무 프로그램의 업무 로직을 담당하는 서비스로 업무 흐름제어, 에러 처리 등의 기능을 제공한다.
데이터처리 서비스는 데이터베이스에 대한 연결 및 영속성 처리, 선언적인 트랜잭션 관리를 지원한다.
연계통합 레이어는 타 시스템과의 연동기능을 지원한다.
배치처리 서비스는 일괄처리 업무 구현에 필요한 기능을 제공한다.
공통기반 서비스는 실행환경 서비스 간에 공통적으로 사용되는 기능을 제공한다.
실행환경의 서비스를 제공하기 위해 필요한 기반 오픈소스 소프트웨어를 도출하고, 오픈소스 소프트웨어 평가 및 테스트를 통하여 서비스별 오픈소스 소프트웨어를 선정하였다.
오픈소스 소프트웨어 평가는 산업 표준, 라이선스, 기능 요건, 성숙도 및 확장 시 지원, 개발 환경 등 다양한 항목에 대한 종합적인 평가를 수행하였다.
표준프레임워크 실행환경 서비스는 선정된 오픈소스 소프트웨어에 기반하여 재활용하거나 확장하여 구현되었으며, 일부 서비스는 선정 기준을 만족하는 오픈소스 소프트웨어가 선정되지 않았으며 자체 구현되었다.
| 서비스 그룹 | 서비스 | 오픈소스 소프트웨어 | 버전 | 확장 및 개발 |
|---|---|---|---|---|
| 화면처리 | Ajax Support | Ajax Tags | 1.5.7 | |
| 화면처리 | Internationalization | Spring MVC | 5.3.27 | |
| 화면처리 | MVC | Spring | 5.3.27 | Custom Tag 외 기능 확장 |
| 화면처리 | Security | Apache Commons Validator | 1.7.0 | |
| 화면처리 | UI Adaptor | 선정되지 않음 | UI Adaptor 연동 매뉴얼 제공 | |
| UX처리 | UX/UI Controller Component | JqueryMobile | 1.4.5 | |
| UX처리 | HTML5 | 선정되지 않음 | HTML5 지원기능 | |
| UX처리 | CSS3 | 선정되지 않음 | CSS3 지원기능 | |
| UX처리 | JavaScript Module App Framework | 선정되지 않음 | UX/UI Controller Component의 효율성을 보장하는 가이드제공 | |
| 업무처리 | Process Control | Spring | 2.4.0 | |
| 업무처리 | Exception Handling | Spring | 5.3.27 | Exception 기능 확장 |
| 데이터처리 | Data Access | iBatis SQL Maps | 2.3.4 | Spring-iBatis 기능 확장 |
| 데이터처리 | Data Access | MyBatis | 3.5.13 | |
| 데이터처리 | DataSource | Spring | 5.3.27 | |
| 데이터처리 | ORM | Hibernate | 5.6.15 | |
| 데이터처리 | Transaction | Spring | 5.3.27 | |
| 연계통합 | Naming Service Support | Spring | 5.3.27 | |
| 연계통합 | Integration Service | 선정되지 않음 | 표준 인터페이스 처리 기능 개발 | |
| 연계통합 | Web Service Interface | CXF | 3.5.6 | 표준 인터페이스를 준수하도록 웹서비스를 확장 |
| 배치처리 | Batch Framework | SpringBatch | 4.3.8 | |
| 공통기반 | AOP | Spring | 5.3.27 | |
| 공통기반 | Cache | EHCache | 2.10.9.2 | |
| 공통기반 | Compress/Decompress | Apache Commons Compress | 1.23.0 | |
| 공통기반 | Encryption/Decryption | java simplified encryption (jasypt) | 1.9.3 | 암호화 기능 확장 |
| 공통기반 | Excel | Apache POI, jXLS | 5.2.3, 2.12.0 | Excel 기능 확장 |
| 공통기반 | File Handling | Jakarta Commons VFS | 2.9.0 | File Access 기능 확장 |
| 공통기반 | File Upload/Download | Apache Commons FileUpload | 1.5.0 | |
| 공통기반 | FTP | Apache Commons Net | 3.9.0 | |
| 공통기반 | ID Generation | 선정되지 않음 | 시스템 고유 ID 생성 기능 개발 | |
| 공통기반 | IoC Container | Spring | 5.3.27 | |
| 공통기반 | Logging | Log4j | 2.20.0 | |
| 공통기반 | Apache Common Email | 1.5.0 | ||
| 공통기반 | Marshalling/Unmarshalling | Apache XML Beans | 5.1.1 | |
| 공통기반 | Object Pooling | Apache Commons Pool | 2.10.0 | |
| 공통기반 | Property | Spring | 5.3.27 | |
| 공통기반 | Resource | Spring | 5.3.27 | |
| 공통기반 | Scheduling | Quartz | 2.3.2 | |
| 공통기반 | Server Security | Spring Security | 5.8.3 | 인증, 권한 관리 기능 확장 |
| 공통기반 | String Util | Jakarta Regexp | 1.4 | 문자열 처리 기능 확장 |
| 공통기반 | XML Manipulation | Apache Xerces 2, JDOM | 2.12.2, 2.0.6.1 | XML 처리 기능 확장 |
Spring IoC Container는 객체(빈) 관리, 의존성 주입, Bean의 초기화와 소멸 등을 제공하며, 다양한 스코프와 프로파일 설정을 지원한다. 또한, Spring은 XML 스키마 기반 AOP, AspectJ 어노테이션, 그리고 리소스를 활용한 메시지 제공 서비스 등을 통해 개발자의 생산성을 높인다.
프레임워크의 기본적인 기능인 Inversion of Control(IoC) Container 기능을 제공하는 서비스이다.
객체의 생성 시, 객체가 참조하고 있는 타 객체에 대한 종속성을 소스 코드 내부에서 하드 코딩하는 것이 아닌, 소스 코드 외부에서 설정하게 함으로써, 유연성 및 확장성을 향상시킨다.
IoC는 Inversion of Control의 약자이다. 우리나라 말로 직역해 보면 “역제어"라고 할 수 있다. 제어의 역전 현상이 무엇인지 살펴본다.
기존에 자바 기반으로 어플리케이션을 개발할 때 자바 객체를 생성하고 서로간의 의존 관계를 연결시키는 작업에 대한 제어권은 보통 개발되는 어플리케이션에 있었다.
그러나, Servlet, EJB 등을 사용하는 경우 Servlet Container, EJB Container에게 제어권이 넘어가서 객체의 생명주기(Life Cycle)를 Container들이 전담하게 된다.
이처럼 IoC에서 이야기하는 제어권의 역전이란 객체의 생성에서부터 생명주기의 관리까지 모든 객체에 대한 제어권이 바뀌었다는 것을 의미한다.
각 클래스 사이의 의존관계를 빈 설정(Bean Definition)정보를 바탕으로 컨테이너가 자동적으로 연결해주는 것을 말한다.
컨테이너가 의존관계를 자동적으로 연결시켜주기 때문에 개발자들이 컨테이너 API를 이용하여 의존관계에 관여할 필요가 없게 되므로 컨테이너 API에 종속되는 것을 줄일 수 있다.
개발자들은 단지 빈 설정파일(저장소 관리 파일)에서 의존관계가 필요하다는 정보를 추가하기만 하면 된다.
본 IoC Container는 Spring Framework의 기능을 수정없이 사용하는 것으로, 본 가이드 문서는 Spring Framework Documentation 을 번역 및 요약한 것이다.
Spring Framework IoC Container에 대한 상세한 설명이 필요한 경우, Spring Framework Documentation 원본 문서 및 Spring Framework API를 참조한다.
org.springframework.beans과 org.springframework.context 패키지는 Spring Framework의 IoC Container의 기반을 제공한다.
BeanFactory 인터페이스는 객체를 관리하기 위한 보다 진보된 설정 메커니즘을 제공한다.
BeanFactory 인터페이스를 기반으로 작성된 ApplicationContext 인터페이스(BeanFactory 인터페이스의 sub-interface이다)는 BeanFactory가 제공하는 기능 외에 Spring AOP, 메시지 리소스 처리(국제화에서 사용됨), 이벤트 전파, 웹 어플리케이션을 위한 WebSpplicationContext 등 어플리케이션 레이어에 특화된 context 등의 기능을 제공한다.
요약하면, BeanFactory는 프레임워크와 기본적인 기능에 대한 설정 기능을 제공하는 반면에, ApplicationContext는 좀더 Enterprise 환경에 맞는 기능들을 추가로 제공한다.
ApplicationContext는 BeanFacatory의 완전한 superset이므로, BeanFactory의 기능 및 행동에 대한 설명은 ApplicationContext에도 모두 해당된다.
본 문서는 크게 두 부분으로 나뉘어지는데, 첫번째 부분은 BeanFactory와 ApplicationContext 모두에 적용되는 기본적인 원리를 설명하고, 두번째 부분은 ApplicationContext에만 적용되는 특징들을 설명한다.
Spring Framework에서 객체가 생성자 인수, 팩토리 메서드에 대한 인수 또는 객체 인스턴스가 생성되거나 팩토리 메서드에서 반환된 후 객체 인스턴스에 설정된 속성을 통해서만 종속성(함께 작업하는 다른 객체)을 정의하는 프로세스를 제어의 역전(Inversion of Control, IoC)라고 한다. 의존성 주입(Dependency Injection, DI)은 모듈간의 의존성을 모듈의 외부 컨테이너 에서 주입시켜주는 기능으로 IoC의 한 종류이다.
Spring Framework에서 Bean은 어플리케이션을 구성하고, IoC Container에 의해 관리되어지는 객체로 간단히 말해 IoC Container에 의해 객체화되고, 조립되고, 또는 관리되는 객체를 의미한다.
Bean들과 Bean들간의 종속성은 Container가 사용하는 설정 메타데이터에 의해 결정된다.
org.springframework.beans.factory.BeanFactory 인터페이스는 Spring IoC Container의 핵심 인터페이스로 Spring IoC Container는 객체를 생성하고, 객체간의 종속성을 이어주는 역할을 한다.

위 그림에서 보듯이, Spring IoC Container는 설정 정보(configuration metadata)를 필요로 한다. 이 설정 정보는 Spring IoC Container가 “객체를 생성하고, 객체간의 종속성을 이어줄 수 있도록” 필요한 정보를 제공한다.
설정 정보는 일반적으로 XML 형태로 작성된다. 설정 정보는 XML 형태가 아닌 Java Annotation을 이용하여 설정이 가능하다.
Annotation을 사용한 설정 방법은 Annotation-based configuration에서 설명하고 있다.
아래 예제는 XML 형태의 설정 정보의 기본적인 모습이다.
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="..." class="...">
<!-- collaborators and configuration for this bean go here -->
</bean>
<bean id="..." class="...">
<!-- collaborators and configuration for this bean go here -->
</bean>
<!-- more bean definitions go here -->
</beans>
<beans> tag는 Spring IoC Container의 설정 정보를 나타내는 tag이다. 그리고 각각의 <bean> tag는 Spring IoC Container가 생성하고, 관리할 객체의 정의를 나타낸다.
XML 설정 정보를 여러 개의 파일로 나뉘어 구성될 수 있다. 이 경우, 전체 설정 정보를 읽기 위해서 하나의 설정 파일에서 다른 파일을 import할 수 있다. Import 하는 방법으로 <import> tag를 사용한다.
<beans>
<import resource="services.xml"/>
<import resource="resources/messageSource.xml"/>
<import resource="/resources/themeSource.xml"/>
<bean id="bean1" class="..."/>
<bean id="bean2" class="..."/>
</beans>
<import> tag의 resource attribute는 import할 XML 설정 파일의 위치를 나타낸다.
다음은 Container를 객체화하는 한 예이다.
ApplicationContext context = new ClassPathXmlApplicationContext(new String[] {"services.xml", "daos.xml"});
// an Application is also a BeanFactory (via inheritance)
BeanFactory factory = context;
위 예제의 ClassPathXmlApplicationContext는 ApplicationContext의 한 종류이며, ApplicationContext 인터페이스는 BeanFactory 인터페이스를 상속하고 있다.
Container를 객체화하면 getBean(String) 메소드를 사용하여 bean을 가져올 수 있다.
Spring IoC Container는 다수의 bean들을 관리한다. Container는 설정 정보를 사용하여 bean들은 생성한다. Container에서 사용하는 bean 정의는 아래 정보를 담고 있다.
클래스 이름(a package-qualified class name): bean의 실제 구현 클래스를 나타낸다.
Bean 행동 정보(bean behavioral configuration elements): Container 안에서 bean이 어떤 식으로 행동하는지에 대한 정보를 나타낸다.(scope, lifecycle callbacks 등등)
다른 bean에 대한 참조(references to other beans): bean이 동작하기 위해 필요한 다른 bean들에 대한 참조 정보를 나타낸다. 이런 참조는 협력자(collaborators) 또는 종속성(dependencies)라고도 한다.
기타 객체에 설정할 정보들(other configuration settings): connection pool을 관리하는 bean에서 사용할 connection의 개수, 또는 pool의 최대 크기 등
위 개념적인 정보들은 실제 <bean> tag로 작성된다. <bean> tag를 구성하는 bean 정의는 아래 표와 같다.
| Feature | Explained in… |
|---|---|
| class | Bean 객체화(Instantiation beans) |
| name | Bean 이름(Naming beans) |
| scope | Bean scope |
| constructor arguments | 종속성 삽입(Injecting dependencies) |
| properties | 종속성 삽입(Injecting dependencies) |
| autowiring mode | 자동엮기(Autowiring collaborators) |
| dependency checking mode | 종속성 검사(Checking for dependencies) |
| lazy-initialization mode | 늦은 객체화(Lazily-instantiated beans) |
| initialization method | 객체화 callbacks(Initialization callbacks) |
| destruction method | 파괴 callbacks(Destruction callbacks) |
모든 bean은 하나 이상의 id를 가져야 하며, 각각의 id는 Container안에서 단 하나만 존재해야 한다. 일반적으로 대부분의 bean은 하나의 id를 가지지만, 별명(alias)를 사용하여 둘 이상의 id를 가질 수도 있다.
Bean id에 대한 명명 규칙은 Java의 class field 명명 규칙과 같다. id는 소문자로 시작하고, 두번째 단어부터는 첫글자는 대문자로 작성한다. ‘accountManager’, ‘accountService’, ‘userDao’, ’loginController’ 등
<alias> tag를 사용하여 이미 정의된 bean에게 추가적인 이름을 부여할 수 있다.
<alias name="fromName" alias="toName"/>
name attribute는 대상이 되는 bean의 이름이고, alias attribute는 부여할 새로운 이름이다.
모든 bean 정의는 객체화를 위해 실제 Java Class가 필요하다.
XML 설정에서는 ‘class’ attribute를 통해 Java Class를 설정한다. 대부분의 경우 Container는 bean를 객체화하기 위해서 Java의 ’new’ 연산자를 사용한다.
또는 특수한 경우, static 메소드를 사용할 수도 있다. 본 문서는 생성자를 이용한 객체화만을 설명한다.
생성자를 이용한 객체화는 가장 일반적인 방식으로, 다음과 같이 사용한다.
<bean id="exampleBean" class="example.ExampleBean"/>
<bean name="anotherExample" class="examples.ExampleBeanTwo"/>
일반적인 엔터프라이즈 애플리케이션은 단일 객체(또는 Spring 용어로 빈)로만 이루어지지 않고 간단한 애플리케이션도 최종 사용자에게 일관된 사용자 경험을 제공하기 위해 여러 객체가 함께 작동한다. 이러한 객체들은 독립적으로 존재하며, Spring 프레임워크를 사용하여 각각의 빈으로 정의된다. 여기서는 독립적으로 정의된 여러 빈들이 협업하여 목표를 달성하는 방법에 대해 설명한다.
종속성 삽입(Dependency Injection(DI))의 기본적인 원칙은 객체는 단지 생성자나 set 메소드를 통해서만 종속성(필요로 하는 객체)를 정의한다는 것이다.
그러면 Container는 Bean 객체를 생성할 때, Bean이 정의한 종속성을 추가하게 되는데 이는 Bean이 스스로 필요한 객체를 생성하거나 찾는 등의 제어를 가지는 것과는 반대의 개념으로 Inversion of Control(IoC)라고 부른다.
종속성 삽입에는 두 가지 방법이 있다. Constructor Injection과 Setter Injection이다.
생성자(Constructor) 기반의 DI는 다수의 arguments를 갖는 생성자를 호출하여 종속성을 주입한다. <constructor-arg> element를 사용한다.
package x.y;
public class Foo {
public Foo(Bar bar, Baz baz) {
// ...
}
}
<beans>
<bean name="foo" class="x.y.Foo">
<constructor-arg>
<bean class="x.y.Bar"/>
</constructor-arg>
<constructor-arg>
<bean class="x.y.Baz"/>
</constructor-arg>
</bean>
</beans>
만약, <value>true</value>와 같이 type이 명확하지 않은 값을 사용하는 경우, Spring은 생성자의 어떤 argument에 해당하는지 결정할 수 없다.
package examples;
public class ExampleBean {
// No. of years to the calculate the Ultimate Answer
private int years;
// The Answer to Life, the Universe, and Everything
private String ultimateAnswer;
public ExampleBean(int years, String ultimateAnswer) {
this.years = years;
this.ultimateAnswer = ultimateAnswer;
}
}
위와 같은 경우, ’type’ attribute를 통해서 각 argument의 타입을 지정할 수 있다.
<bean id="exampleBean" class="examples.ExampleBean">
<constructor-arg type="int" value="7500000"/>
<constructor-arg type="java.lang.String" value="42"/>
</bean>
위와 같은 경우 ‘index’ attribute를 통해서 각 argument의 위치를 지정할 수 있다.
<bean id="exampleBean" class="examples.ExampleBean">
<constructor-arg index="0" value="7500000"/>
<constructor-arg index="1" value="42"/>
</bean>
(* index는 0부터 시작한다.)
Setter 기반의 DI는 argument가 없는 생성자를 통해 bean 객체가 생성된 후, setter 메소드를 호출하여 종속성을 주입한다. <property> element를 사용한다.
<bean id="exampleBean" class="examples.ExampleBean">
<!-- setter injection using the nested <ref/> element -->
<property name="beanOne"><ref bean="anotherExampleBean"/></property>
<!-- setter injection using the neater 'ref' attribute -->
<property name="beanTwo" ref="yetAnotherBean"/>
<property name="integerProperty" value="1"/>
</bean>
<bean id="anotherExampleBean" class="examples.AnotherBean"/>
<bean id="yetAnotherBean" class="examples.YetAnotherBean"/>
public class ExampleBean {
private AnotherBean beanOne;
private YetAnotherBean beanTwo;
private int i;
public void setBeanOne(AnotherBean beanOne) {
this.beanOne = beanOne;
}
public void setBeanTwo(YetAnotherBean beanTwo) {
this.beanTwo = beanTwo;
}
public void setIntegerProperty(int i) {
this.i = i;
}
}
본 장은 종속성 삽입에 사용되는 <constructor-arg>와 <property> element의 sub-element type을 설명한다.
사람이 인식 가능한 문자열 형태를 <value> tag를 사용하여 표현한다. String을 argument나 property의 type에 맞춰 변환해준다.
<bean id="myDataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
<!-- results in a setDriverClassName(String) call -->
<property name="driverClassName">
<value>com.mysql.jdbc.Driver</value>
</property>
<property name="url">
<value>jdbc:mysql://localhost:3306/mydb</value>
</property>
<property name="username">
<value>root</value>
</property>
<property name="password">
<value>masterkaoli</value>
</property>
</bean>
<value> element 대신 ‘value’ attribute를 사용할 수도 있다.
<bean id="myDataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
<!-- results in a setDriverClassName(String) call -->
<property name="driverClassName" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/mydb"/>
<property name="username" value="root"/>
<property name="password" value="masterkaoli"/>
</bean>
ref element는 Container 안에 있는 다른 bean을 참조한다. 참조할 객체를 지정하는 방식에는 3가지가 있다.
bean attribute
가장 일반적인 형태로 같은 Container 또는 부모 Container에 포함된 bean 객체를 참조한다. ‘bean’ attribute는 대상 bean의 ‘id’ 또는 여러 ’name’들 중. 하나와 같아야 한다.
<ref bean="someBean"/>
local attribute
같은 XML 설정 파일 내의 bean 객체를 참조한다. ’local’ attribute는 반드시 대상 bean의 ‘id’와 같아야 한다. 만약 대상 bean이 같은 XML 파일에 존재한다면. local을 사용하는 것이 좋다.
<ref local="someBean"/>
parent attribute
현재 Container의 부모 Container의 bean 객체를 참조한다. ‘parent’ attribute는 대상 bean의 ‘id’ 또는 여러 ’name’들 중 하나와 같아야 한다.
<!-- in the parent context -->
<bean id="accountService" class="com.foo.SimpleAccountService">
<!-- insert dependencies as required as here -->
</bean>
<!-- in the child (descendant) context -->
<bean id="accountService" <-- notice that the name of this bean is the same as the name of the 'parent' bean
class="org.springframework.aop.framework.ProxyFactoryBean">
<property name="target">
<ref parent="accountService"/> <-- notice how we refer to the parent bean
</property>
<!-- insert other configuration and dependencies as required as here -->
</bean>
<property/> 또는 <constructor-arg/> element 안에 있는 <bean/> element를 inner bean이라고 한다. Inner bean은 id나 name을 정의할 필요가 없다. 정의한다 해도 Container에서 무시하기 때문에 정의하지 않는 것이 좋다.
<bean id="outer" class="...">
<!-- instead of using a reference to a target bean, simply define the target bean inline -->
<property name="target">
<bean class="com.example.Person"> <!-- this is the inner bean -->
<property name="name" value="Fiona Apple"/>
<property name="age" value="25"/>
</bean>
</property>
</bean>
Inner bean의 ‘scope’ flag와 ‘id’, ’name’은 무시된다. Inner bean의 scope은 항상 prototype이다. 따라서 inner bean을 다른 bean에 주입하는 것은 불가능한다.
Java Collection 타입인 List, Set, Map, Properties를 표현하기 위해 <list/>, <set/>, <map/>, <props/> element가 사용된다.
<bean id="moreComplexObject" class="example.ComplexObject">
<!-- results in a setAdminEmails(java.util.Properties) call -->
<property name="adminEmails">
<props>
<prop key="administrator">administrator@example.org</prop>
<prop key="support">support@example.org</prop>
<prop key="development">development@example.org</prop>
</props>
</property>
<!-- results in a setSomeList(java.util.List) call -->
<property name="someList">
<list>
<value>a list element followed by a reference</value>
<ref bean="myDataSource" />
</list>
</property>
<!-- results in a setSomeMap(java.util.Map) call -->
<property name="someMap">
<map>
<entry>
<key>
<value>an entry</value>
</key>
<value>just some string</value>
</entry>
<entry>
<key>
<value>a ref</value>
</key>
<ref bean="myDataSource" />
</entry>
</map>
</property>
<!-- results in a setSomeSet(java.util.Set) call -->
<property name="someSet">
<set>
<value>just some string</value>
<ref bean="myDataSource" />
</set>
</property>
</bean>
map의 key와 value, set의 value의 값은 아래 element 중 하나가 될 수 있다.
bean | ref | idref | list | set | map | props | value | null
Container는 collection 병합 기능을 제공한다. Bean 정의 상속을 사용하여 부모 bean 정의의 <list/>, <map/>, <set/>, <props/> element와 자식 bean 정의의 <list/>, <map/>, <set/>, <props/> element를 병합할 수 있다.
<beans>
<bean id="parent" abstract="true" class="example.ComplexObject">
<property name="adminEmails">
<props>
<prop key="administrator">administrator@example.com</prop>
<prop key="support">support@example.com</prop>
</props>
</property>
</bean>
<bean id="child" parent="parent">
<property name="adminEmails">
<!-- the merge is specified on the *child* collection definition -->
<props merge="true">
<prop key="sales">sales@example.com</prop>
<prop key="support">support@example.co.uk</prop>
</props>
</property>
</bean>
<beans>
위 설정에 따라 생성된 child bean 객체의 adminEmails는 아래와 같은 값을 가진다.
administrator=administrator@example.com
sales=sales@example.com
support=support@example.co.uk
null 값을 사용하기 위해서 <null/> element를 사용한다. Spring는 argument가 없을 경우 빈 문자열(””)로 인식한다.
<bean class="ExampleBean">
<property name="email"><value/></property>
</bean>
위 설정에 따르면, email의 값은 ”“이다. 다음은 null값을 갖는 예제이다.
<bean class="ExampleBean">
<property name="email"><null/></property>
</bean>
<property/>, <constructor-arg/>, <entry/> element는 모두 <value/> element 대신에 ‘value’ attribute를 사용할 수 있다.
<property name="myProperty">
<value>hello</value>
</property>
<constructor-arg>
<value>hello</value>
</constructor-arg>
<entry key="myKey">
<value>hello</value>
</entry>
위 설정은 아래와 동일한 설정이다.
<property name="myProperty" value="hello"/>
<constructor-arg value="hello"/>
<entry key="myKey" value="hello"/>
<property/>, <constructor-arg/> element는 <ref/> element 대신에 ‘ref’ attribute를 사용할 수 있다.
<property name="myProperty">
<ref bean="myBean">
</property>
<constructor-arg>
<ref bean="myBean">
</constructor-arg>
위 설정은 아래와 동일한 설정이다.
<property name="myProperty" ref="myBean"/>
<constructor-arg ref="myBean"/>
단, shortcut은 <ref bean=“xxx”>와 동일하다. <ref local=“xxx”>에 해당하는 shortcut은 없다.
<entry/> element는 ‘key’ / ‘key-ref’와 ‘value’ / ‘value-ref’ attribute를 사용할 수 있다.
<entry>
<key>
<ref bean="myKeyBean" />
</key>
<ref bean="myValueBean" />
</entry>
위 설정은 아래와 같은 설정이다.
<entry key-ref="myKeyBean" value-ref="myValueBean"/>
‘’<property/>’’ element 대신 “p-namespace”를 사용하여 XML 설정을 작성할 수 있다. 아래 classic bean과 p-namespace bean은 동일한 Bean 설정이다.
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean name="classic" class="com.example.ExampleBean">
<property name="email" value="foo@bar.com/>
</bean>
<bean name="p-namespace"
class="com.example.ExampleBean"
p:email="foo@bar.com"/>
</beans>
아래 예제는 다른 bean 객체의 참조를 삽입하는 예제이다. Attribute 이름 끝에 ‘-ref’를 붙이면 참조로 인식한다.
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean name="john-classic" class="com.example.Person">
<property name="name" value="John Doe"/>
<property name="spouse" ref="jane"/>
</bean>
<bean name="john-modern"
class="com.example.Person"
p:name="John Doe"
p:spouse-ref="jane"/>
<bean name="jane" class="com.example.Person">
<property name="name" value="Jane Doe"/>
</bean>
</beans>
복합 형식의 property 이름도 사용 가능하다.
<bean id="foo" class="foo.Bar">
<property name="fred.bob.sammy" value="123" />
</bean>
foo bean은 fred property를 가지고, fred property는 bob property를 가진다. 그리고 bob property는 sammy property를 가지고, 마지막 sammy property가 123을 값으로 가진다. 이 작업이 정상적으로 동작하려면 bean이 생성되었을 때, foo의 fred property, fred의 bob property는 반드시 null이 아니어야 한다. 그렇지 않을 경우 NullPointerException이 발생한다.
대부분의 경우, bean들간의 종속성은 ‘’<ref/>’’ element에 의해 표현된다. 하지만 드물게 이런 종속성이 직접 나타나지 않는 경우도 있다(예를 들면, database driver 등록처럼 static 메소드에 의해 초기화되어야 하는 경우 등). 이런 경우 ‘depends-on’ attribute를 사용하여 명시적으로 종속성을 표현할 수 있다.
<bean id="beanOne" class="ExampleBean" depends-on="manager"/>
<bean id="manager" class="ManagerBean" />
만약 다수의 bean에 대한 종속성을 표현하고 하는 경우에는, ‘depends-on’ attribute의 값으로 bean 이름을 나열하면 된다. bean 이름의 구분자로는 콤마(’,’), 공백문자(’ ‘), 세미콜론(’;’) 등을 사용할 수 있다.
<bean id="beanOne" class="ExampleBean" depends-on="manager,accountDao">
<property name="manager" ref="manager" />
</bean>
<bean id="manager" class="ManagerBean" />
<bean id="accountDao" class="x.y.jdbc.JdbcAccountDao" />
ApplicationContext는 시작시에 모든 singleton bean을 선객체화(pre-instantiate)한다. 선객체화(pre-instantiate)는 초기화 과정에서 모든 singleton baen을 생성하고 설정한다는 것을 의미한다. 일반적으로 선객체화가 좋은 방식인데, 왜냐하면 잘못된 설정이 있는 경우, 즉시 발견할 수 있기 때문이다.
어쨌거나, 이런 방식을 원하지 않을 경우도 있다. 만약 ApplicationContext에 의해 선 객체화 되는 singleton bean을 원하지 않을 경우, 선택적으로 bean 정의에 늦은 객체화(lazy-initailized)를 설정할 수 있다. 늦은 객체화(lazy-initailized)로 설정된 bean은 시작 시에 생성되는 것이 아니라, 처음으로 필요로 했을 때 생성된다.
XML 설정에서는 ‘’<bean/>’’ element의 ’lazy-init’ attribute를 사용한다.
<bean id="lazy" class="com.foo.ExpensiveToCreateBean" lazy-init="true"/>
<bean name="not.lazy" class="com.foo.AnotherBean"/>
늦은 객체화에 대해서 이해하고 있어야 하는 것은, 만약 늦은 객체화로 설정된 bean에 대해서 그렇지 않은 singleton bean이 종속성을 가지고 있다면, ApplicationContext는 시작 시에 singleton bean이 종속하고 있는 모든 bean을 생성한다는 것이다. 즉, 명시적으로 늦은 객체화로 선언한 bean이라도 시작 시에 생성될 수 있다.
그리고, <beans/> element의 ‘default-lazy-init’ attribute를 사용하여 Container 레벨에서의 늦은 객체화를 설정할 수 있다.
<beans default-lazy-init="true">
<!-- no beans will be pre-instantiated... -->
</beans>
Spring Container는 서로 관계된 bean들을 자동으로 엮어(autowire)줄 수 있다. 자동엮기(autowiring)는 각각의 bean 단위로 설정된다. 자동엮기(autowiring) 기능을 사용하면 property나 생성자 argument를 지정할 필요가 없어지므로, 타이핑일 줄일 수 있다. 자동엮기(autowiring)에는 5가지 모드가 있으며, XML 기반 설정에서는 <bean/> element의 ‘autowire’ attribute를 사용하여 설정할 수 있다.
| Mode | 설명 |
|---|---|
| no | 자동엮기를 사용하지 않는다. Bean에 대한 참조는 ref element를 사용하여 지정해야만 한다. 이 모드가 기본(default)이다. |
| byName | Property 이름으로 자동엮기를 수행한다. Property의 이름과 같은 이름을 가진 bean을 찾아서 엮어준다. |
| byType | Property 타입으로 자동엮기를 수행한다. Property의 타입과 같은 타입을 가진 bean을 찾아서 엮어준다. 만약 같은 타입을 가진 bean이 Container에 둘 이상 존재할 경우 exception이 발생한다. 만약 같은 타입을 가진 bean이 존재하지 않는 경우, 아무 일도 발생하지 않는다; 즉, property에는 설정되지 않는다. |
| constructor | byType과 유사하지만, 생성자 argument에만 적용된다. 만약 같은 타입의 bean이 존재하지 않거나 둘 이상 존재할 경우, exception이 발생한다. |
| autodetect | Bean class의 성질에 따라 constructor와 byType 모드 중 하나를 선택한다. 만약 default 생성자가 존재하면, byType 모드가 적용된다. |
만약 종속성을 property나 constructor-arg를 사용하여 명시적으로 설정한 경우, 자동엮기(autowiring) 설정은 무시된다.
<bean/> element의 ‘autowire-candidate’ attribute 값을 ‘false’로 설정함으로써, 대상 bean이 다른 bean에 의해 자동엮임을 당하는 것을 방지할 수 있다.
Spring IoC Container는 bean의 미해결 종속성의 존재를 검사할 수 있다. 이 기능은 bean의 모든 property가 지정되었는지는 확인하고 싶을 때 유용하다. 종속성 검자(Dependency checking) 기능은 자동엮기(autowiring) 기능과 마찬가지로 각각의 bean마다 설정할 수 있다. 종속성 검사에는 4가지 모드가 있으며, XML 기반 설정에서는 <bean/> element의 ‘dependency-check’ attribute를 사용하여 설정할 수 있다.
| Mode | 설명 |
|---|---|
| none | 종속성 검사를 하지 않는다. 기본(default) 모드이다. |
| simple | Primitive 타입과 collection에 대해서 종속성 검사를 수행한다. |
| object | 관련된 객체에 대해서만 종속성 검사를 수행한다. |
| all | Primitive 타입과 collection, 관련된 객체에 대해서 종속성 검사를 수행한다. |
대부분의 어플리케이션에서, Container에 존재하는 대부분의 bean은 singleton이다. Singleton bean이 다른 singleton bean과 협력(collaborate)하거나, non-singleton bean이 다른 non-singleton bean과 협력하는 경우, 가장 일반적인 방법은 bean의 property를 정의함으로써 종속성을 조절하는 것이다. 하지만 만약 관련된 bean들의 생명주기가 다른 경우 문제가 발생한다. Singleton bean A가 non-singleton bean B를 사용한다고 할 때, Container는 singleton bean A를 단지 한번만 생성할 것이고, 따라서 property도 역시 한번만 설정될 것이다. Container는 bean B가 필요한 매 순간 새로운 객체를 생성하여 bean A에게 제공해야 하지만, 그럴 수 있는 방법이 없다.
위 문제에 대한 한가지 해법은 몇몇 제어의 역전(inversion of control)를 버리는 것이다. Bean A는 BeanFactoryAware interface를 구현함으로써 자신이 속한 Container를 알 수 있다. 그리고 bean B의 객체가 필요한 순간에 Container의 getBean(“B”)을 호출함으로써 bean B의 객체를 가져올 수 있다.
// a class that uses a stateful Command-style class to perform some processing
package fiona.apple;
// lots of Spring-API imports
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
public class CommandManager implements BeanFactoryAware {
private BeanFactory beanFactory;
public Object process(Map commandState) {
// grab a new instance of the appropriate Command
Command command = createCommand();
// set the state on the (hopefully brand new) Command instance
command.setState(commandState);
return command.execute();
}
// the Command returned here could be an implementation that executes asynchronously, or whatever
protected Command createCommand() {
return (Command) this.beanFactory.getBean("command"); // notice the Spring API dependency
}
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
this.beanFactory = beanFactory;
}
}
위 예제는 일반적으로는 바람직하지 않은 솔루션이다. 왜냐하면 업무 코드(business code)는 Spring Framework과 관련될 필요가 없기 때문이다. 메소드 삽입(Method Injection)은 이런 경우를 말끔히 해결할 수 있는 방법이다.
Lookup 메소드 삽입은 Container가 관리하고 있는 bean의 메소드를 덮어써서(override) Container 안에 있는 다른 bean을 찾을 수 있게 하는 기능이다. Spring Framework는 메소드 삽입을 구현하기 위해서 CGLIB 라이브러리를 사용하여 동적으로 상속클래스를 생성한다.
package fiona.apple;
// no more Spring imports!
public abstract class CommandManager {
public Object process(Object commandState) {
// grab a new instance of the appropriate Command interface
Command command = createCommand();
// set the state on the (hopefully brand new) Command instance
command.setState(commandState);
return command.execute();
}
// okay... but where is the implementation of this method?
protected abstract Command createCommand();
}
삽입될 메소드는 반드시 다음과 같은 형태를 가져야 한다.
<public|protected> [abstract] <return-type> theMethodName(no-arguments);
만약 메소드가 abstract이면, 동적으로 생성된 서브클래스는 메소드를 구현할 것이다. 만약 그렇지 않으면 동적으로 생성된 서브클래스를 원본 클래스의 메소드를 덮어쓸(override) 것이다.
<!-- a stateful bean deployed as a prototype (non-singleton) -->
<bean id="command" class="fiona.apple.AsyncCommand" scope="prototype">
<!-- inject dependencies here as required -->
</bean>
<!-- commandProcessor uses statefulCommandHelper -->
<bean id="commandManager" class="fiona.apple.CommandManager">
<lookup-method name="createCommand" bean="command"/>
</bean>
commandManager는 command bean의 새로운 객체가 필요할 때마다 자신의 createCommand() 메소드를 호출할 것이다. 만약 command bean이 prototype이 아닌 singleton인 경우, createCommand 메소드는 같은 객체를 리턴할 것이다.
동적 서브클래스 생성이 동작하려면 classpath에 CGLIB가 추가되어 있어야 한다. 그리고 원본 class는 final이면 안되며, 덮어쓸(override) 메소드 역시 final이면 안된다.
Bean 정의는 실제 Bean 객체를 생성하는 방식을 정의하는 것으로 Class와 마찬가지로 하나의 Bean 정의에 해당하는 다수의 객체가 생성될 수 있다.
Bean 정의를 통해 객체에 다양한 종속성 및 설정값을 주입할 수 있을 뿐 아니라, 객체의 범위(Scope)를 정의할 수 있다.
Spring 프레임워크는 6개의 Scope를 지원하며, 이 중 4개의 Scope는 Web-aware ApplicationContext를 사용하는 경우에만 사용할 수 있다. 또한, 사용자 정의 범위를 생성할 수도 있다.
| Scope | 설명 |
|---|---|
| singleton | 하나의 Bean 정의에 대해서 Spring IoC Container 내에 단 하나의 객체만 존재한다. |
| prototype | 하나의 Bean 정의에 대해서 다수의 객체가 존재할 수 있다. |
| request | 하나의 Bean 정의에 대해서 하나의 HTTP request 생명주기 안에 단 하나의 객체만 존재한다. 즉, 각각의 HTTP Request는 자신만의 객체를 가진다. web-aware Spring ApplicationContext 안에서만 유효하다. |
| session | 하나의 Bean 정의에 대해서 하나의 HTTP Session 생명주기 안에 단 하나의 객체만 존재한다. web-aware Spring ApplicationContext 안에서만 유효하다. |
| application | 하나의 Bean 정의에 대해서 하나의 ServletContext 생명주기 안에 단 하나의 객체만 존재한다. web-aware Spring ApplicationContext 안에서만 유효하다. |
| websocket | 하나의 Bean 정의에 대해서 하나의 Websocket 생명주기 안에 단 하나의 객체만 존재한다. web-aware Spring ApplicationContext 안에서만 유효하다. |
Bean이 singleton인 경우, 단지 하나의 공유 객체만 관리된다.

Singleton scope은 Spring의 기본(default) scope이다.
<bean id="accountService" class="com.something.DefaultAccountService"/>
<!-- the following is equivalent, though redundant (singleton scope is the default) -->
<bean id="accountService" class="com.something.DefaultAccountService" scope="singleton"/>
Singleton이 아닌 prototype scope의 형태로 정의된 bean은 필요한 매 순간 새로운 bean 객체가 생성된다.

<bean id="accountService" class="com.something.DefaultAccountService" scope="prototype"/>
Prototype scope을 사용할 때 염두에 두고 있어야 할 것이 있다.
Spring은 prototype bean의 전체 생명주기를 관리하지 않는데 Container는 객체화하고, 값을 설정하고, 다른 prototype 객체와 조립하여 Client에게 전달한 후 더 이상 prototype 객체를 관리하지 않는다.
즉, scope에 관계없이 초기화(initialization) 생명주기 callback 메소드가 호출되는 반면에, prototype의 경우 파괴(destruction) 생명주기 callback 메소드는 호출되지 않는다.
이것은 client 코드가 prototype 객체를 clean up하고 prototype 객체가 들고 있던 리소스를 해제하는 책임을 가진다는 것을 의미한다.
이 문제는 메소드 삽입(Method Injection)에서 다루고 있다.
request, session, application, websocket scope들은 반드시 web-based 어플리케이션에서 사용할 수 있다.
request, session, application, websocket scope을 사용하기 위해서는 추가적인 설정이 필요하다. 추가 설정은 사용할 Servlet 환경에 따라 달라진다.
만약 Spring Web MVC 안에서 bean에 접근할 경우, 즉 Spring DispatcherServlet 또는 DispatcherPortlet에서 처리되는 요청인 경우, 별도의 추가 설정은 필요없다.( DispatcherServlet과 DispatcherPortlet은 이미 모든 관련있는 상태를 제공한다.)
만약 Servlet 2.4+ web Container를 사용하고, JSF나 Struts 등과 같이 Spring의 DispatcherServlet의 외부에서 요청을 처리하는 경우, 다음 javax.servlet.ServletRequestListener를 ‘web.xml’ 파일에 추가해야 한다.
<web-app>
...
<listener>
<listener-class>org.springframework.web.context.request.RequestContextListener</listener-class>
</listener>
...
</web-app>
만약 다른 오래된 web Container(Servlet 2.3)를 사용한다면, 제공되는 javax.servlet.Filter 구현체를 사용해야 한다.(filter mapping은 web 어플리케이션 설정에 따라 달라질 수 있으므로, 적절히 수정해야 한다.)
<web-app>
..
<filter>
<filter-name>requestContextFilter</filter-name>
<filter-class>org.springframework.web.filter.RequestContextFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>requestContextFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
...
</web-app>
<bean id="loginAction" class="com.foo.LoginAction" scope="request"/>
위 정의에 따라, Spring Container는 모든 HTTP request에 대해서 ’loginAction’ bean 정의에 대한 LoginAction 객체를 생성할 것이다. 즉, ’loginAction’ bean은 HTTP request 수준에 한정된다(scoped). 요청에 대한 처리가 완료되었을 때, 한정된(scoped) bean도 폐기된다.
<bean id="userPreferences" class="com.foo.UserPreferences" scope="session"/>
위 정의에 따라, Spring Container는 하나의 HTTP Session 일생동안 ‘userPreferences’ bean 정의에 대한 UserPreferences 객체를 생성할 것이다. 즉, ‘userPreferences’ bean은 HTTP Session 수준에 한정된다(scoped). HTTP Session이 폐기될 때, 한정된(scoped) bean로 폐기된다.
<bean id="appPreferences" class="com.foo.AppPreferences" scope="application"/>
위 정의에 따라, Spring Container는 전체 Web Application에 대해 ‘appPreferences’ bean 정의에 대한 AppPreferences 객체를 생성할 것이다. singleton scope와 비슷하지만 bean의 scope와 관련하여 매우 중요한 차이가 있는데, bean이 application 범위인 경우 bean의 동일한 instance는 동일한 ServletContext에서 실행되는 여러 서블릿 기반 애플리케이션에서 공유되는 반면 singleton 범위의 bean은 단일 애플리케이션 컨텍스트로만 범위가 지정된다.
<bean id="appPreferences" class="com.foo.AppPreferences" scope="websocket"/>
WebSocket scope bean은 WebSocket 세션 속성에 저장된다. 그리고 전체 WebSocket 세션 동안 해당 bean에 액세스할 때마다 bean의 동일한 instance가 반환된다.
HTTP request 또는 Session에 한정적인(scoped) bean을 정의하는 것은 꽤 괜찮은 기능이지만 Spring IoC Container가 제공하는 핵심 기능은 객체를 생성하는 것 뿐만 아니라 엮어준다는 것이다. 만약 HTTP request에 한정적인(scoped) bean을 다른 bean에 주입하기를 원한다면, 한정적(scoped) bean 대신에 AOP Proxy를 주입해야 한다.
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">
<!-- a HTTP Session-scoped bean exposed as a proxy -->
<bean id="userPreferences" class="com.foo.UserPreferences" scope="session">
<!-- this next element effects the proxying of the surrounding bean -->
<aop:scoped-proxy/>
</bean>
<!-- a singleton-scoped bean injected with a proxy to the above bean -->
<bean id="userService" class="com.foo.SimpleUserService">
<!-- a reference to the proxied 'userPreferences' bean -->
<property name="userPreferences" ref="userPreferences"/>
</bean>
</beans>
Proxy를 생성하기 위해서 <aop:scoped-proxy/> element를 scoped bean 정의에 추가해야 한다(CGLIB 라이브러리도 classpath에 추가해야 한다).
InitializingBean과 DisposableBean 인터페이스를 구현할 수 있다. 컨테이너는 빈이 초기화될 때 afterPropertiesSet() 메서드를, 소멸될 때 destroy() 메서드를 호출하여 특정 작업을 수행하도록 한다.컨테이너의 빈 라이프사이클 관리와 상호 작용하기 위해 Spring InitializingBean 및 DisposableBean 인터페이스를 구현할 수 있는데, 컨테이너는 전자의 경우 afterPropertiesSet()을 호출하고 후자의 경우 destroy()를 호출하여 빈이 초기화 및 소멸될 때 특정 작업을 수행하도록 한다.
Spring Framework는 Container 내부의 bean의 행동을 변화시길 수 있는 다양한 callback interface를 제공한다.
org.springframework.beans.factory.InitializingBean interface를 구현하면 bean에 필요한 모든 property를 설정한 후, 초기화 작업을 수행한다. InitializingBean interface는 다음 메소드를 명시하고 있다.
void afterPropertiesSet() throws Exception;
일반적으로, InitializingBean interface의 사용을 권장하지 않는다. 왜냐하면 code가 불필요하게 Spring과 결합되기(couple) 때문이다. 대안으로, bean 정의는 초기화 메소드를 지정할 수 있다. XML 기반 설정의 경우, ‘init-method’ attribute를 사용한다.
<bean id="exampleInitBean" class="examples.ExampleBean" init-method="init"/>
public class ExampleBean {
public void init() {
// do some initialization work
}
}
위 예제는 아래 예제와 같다.
<bean id="exampleInitBean" class="examples.AnotherExampleBean"/>
public class AnotherExampleBean implements InitializingBean {
@Override
public void afterPropertiesSet() {
// do some initialization work
}
}
org.springframework.beans.factory.DisposableBean interface를 구현하면, Container가 파괴될 때 bean이 callback를 받을 수 있다. DisposableBean interface는 다음 메소드를 명시하고 있다.
void destroy() throws Exception;
일반적으로, DisposableBean interface의 사용을 권장하지 않는다. 왜냐하면 code가 불필요하게 Spring과 결합되기(couple) 때문이다. 대안으로, bean 정의는 초기화 메소드를 지정할 수 있다. XML 기반 설정의 경우, ‘destroy-method’ attribute를 사용한다.
<bean id="exampleInitBean" class="examples.ExampleBean" destroy-method="cleanup"/>
public class ExampleBean {
public void cleanup() {
// do some destruction work (like releasing pooled connections)
}
}
위 예제는 아래 예제와 같다.
<bean id="exampleInitBean" class="examples.AnotherExampleBean"/>
public class AnotherExampleBean implements DisposableBean {
@Override
public void destroy() {
// do some destruction work (like releasing pooled connections)
}
}
Spring Container는 모든 bean에 대해서 같은 이름의 초기화 및 파괴 메소드를 지정할 수 있다.
public class DefaultBlogService implements BlogService {
private BlogDao blogDao;
public void setBlogDao(BlogDao blogDao) {
this.blogDao = blogDao;
}
// this is (unsurprisingly) the initialization callback method
public void init() {
if (this.blogDao == null) {
throw new IllegalStateException("The [blogDao] property must be set.");
}
}
}
<beans default-init-method="init">
<bean id="blogService" class="com.foo.DefaultBlogService">
<property name="blogDao" ref="blogDao" />
</bean>
</beans>
<beans/> element의 ‘default-init-method’ attribute를 이용하여 기본 객체화 callback 메소드를 지정할 수 있다. 파괴 callback 메소드의 경우 ‘default-destroy-method’ attribute를 이용하여 지정할 수 있다.
<bean/> element에 ‘init-method’, ‘destroy-method’ attribute가 정의되어 있는 경우, 기본값은 무시된다.
Spring Framework에서는 3가지 방식의 생명주기 메커니즘이 존재한다: InitialzingBean과 DisposableBean interface; 맞춤 init()과 destroy() 메소드; 그리고 @PostConstruct and @PreDestroy annotations
만약 서로 다른 생명주기 메커니즘을 같이 사용할 경우, 개발자는 적용되는 순서를 알고 있어야 한다. 객체화 메소드의 순서는 다음과 같다.
@PostConstruct annotation이 있는 메소드
InitializingBean callback interface에 정의된 afterPropertiesSet()
맞춤 init() 메소드
파괴 메소드의 호출 순서는 다음과 같다.
@PreDestroy annotation이 있는 메소드
DisposableBean callback interface에 정의된 destroy()
맞춤 destroy() 메소드
Lifecycle 인터페이스는 자체 수명 주기 요구 사항(예: 일부 백그라운드 프로세스 시작 및 중지)이 있는 모든 객체에 대해 필수 메서드를 정의할 수 있다.
public interface Lifecycle {
void start();
void stop();
boolean isRunning();
}
모든 Spring 관리 객체는 Lifecycle 인터페이스를 구현할 수 있다. 그런 다음 애플리케이션 컨텍스트 자체가 시작 및 중지 신호를 수신하면(예: 런타임에 중지/재시작 시나리오의 경우) 해당 컨텍스트 내에 정의된 모든 Lifecycle 구현으로 해당 호출을 캐스케이드한다.
public interface LifecycleProcessor extends Lifecycle {
void onRefresh();
void onClose();
}
NoSuchBeanDefinitionException이 발생한다.Bean의 Profile은 Spring f/w ver. 3.1부터 추가되었으며 동일한 id의 bean을 여러 개 정의하여 사용자의 설정으로 활성화시킨 Profile의 해당 bean이 Runtime시에 동작하도록 하는 기능이다. 보통 개발시점과 운영시점에 bean의 Profile설정 변경만으로 Spring Container에서 Bean적용이 달리 적용되도록 하는데 쓰인다.
Profile설정 시, 반드시 Profile을 활성화해야만 사용가능하다. 만약 Profile만 설정하고 활성화하지 않으면 Exeption(NoSuchBeanDefinitionException)이 발생한다.
아래에서 Profile을 설정하는 방법과 Profile을 활성화(Active Profile)하는 방법에 대하여 알아본다.
Profile의 설정방법에는 XML설정과 Annotation설정으로 나뉜다.
기존의 전형적인 XML Bean설정에 대하여 알아보고 새로 추가된 Profiles설정에 대하여 살펴본다. 동일한 Bean id를 Profile별로 XML에 설정하는 방법에는 XML을 나누는 방법, 하나의 XML에서 관리하는 방법이 있다
기존에는 Bean id설정은 반드시 유일해야 하며 Bean설정을 변경하기 위해서는 다른 id를 가진 Bean을 새로 설정하거나 동일 Bean의 내부설정을 변경해주어야했다.
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:jdbc="http://www.springframework.org/schema/jdbc"
xsi:schemaLocation="...">
<bean id="transferService" class="com.bank.service.internal.DefaultTransferService">
<constructor-arg ref="accountRepository"/>
<constructor-arg ref="feePolicy"/>
</bean>
<bean id="accountRepository" class="com.bank.repository.internal.JdbcAccountRepository">
<constructor-arg ref="dataSource"/>
</bean>
<bean id="feePolicy" class="com.bank.service.internal.ZeroFeePolicy"/>
<jdbc:embedded-database id="dataSource">
<jdbc:script location="classpath:com/bank/config/sql/schema.sql"/>
<jdbc:script location="classpath:com/bank/config/sql/test-data.sql"/>
</jdbc:embedded-database>
</beans>
1. Bean Profiles - Xml을 분리하여 설정하는 방법
<transfer-service-config.xml>
아래는 “dataSource” bean을 사용하는 “accountRepository” bean 설정한 XML이다. 어떤 Profile의 bean인지 여부와 상관없이 Spring container기동시점에는 dataSource라는 bean id를 가진 유일한 bean을 가져오기 때문에 이전 설정과 똑같이 bean id값만 쓰면 된다.
<beans ...>
<bean id="transferService" ... />
<bean id="accountRepository" class="com.bank.repository.internal.JdbcAccountRepository">
<constructor-arg ref="dataSource"/>
</bean>
<bean id="feePolicy" ... />
</beans>
<standalone-datasource-config.xml>
개발시점에 사용하는 “dataSource” bean을 정의하는 XML. Profile명은 “dev"로 정의하고 있으며 Embedded DB를 DataSource로 설정하고 있다. “dev” Profile을 활성화시키면 해당Bean이 동작한다.
<beans profile="dev">
<jdbc:embedded-database id="dataSource">
<jdbc:script location="classpath:com/bank/config/sql/schema.sql"/>
<jdbc:script location="classpath:com/bank/config/sql/test-data.sql"/>
</jdbc:embedded-database>
</beans>
<jndi-datasource-config.xml>
운영시점에 사용하는 “dataSource” bean을 정의하는 XML. Profile명은 “production"으로 정의하고 있으며 JDNI를 DataSource로 설정하고 있다. “production” Profile을 활성화시키면 해당 Bean이 동작한다.
<beans profile="production">
<jee:jndi-lookup id="dataSource" jndi-name="java:comp/env/jdbc/datasource"/>
</beans>
2. Bean Profiles - XML을 한 파일에서 설정
<beans> element를 하나의 XML파일에서 profile값과 함께 여러 번 정의할 수도 있다.
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:jdbc="http://www.springframework.org/schema/jdbc"
xmlns:jee="http://www.springframework.org/schema/jee"
xsi:schemaLocation="...">
<bean id="transferService" class="com.bank.service.internal.DefaultTransferService">
<constructor-arg ref="accountRepository"/>
<constructor-arg ref="feePolicy"/>
</bean>
<bean id="accountRepository" class="com.bank.repository.internal.JdbcAccountRepository">
<constructor-arg ref="dataSource"/>
</bean>
<bean id="feePolicy" class="com.bank.service.internal.ZeroFeePolicy"/>
<beans profile="dev">
<jdbc:embedded-database id="dataSource">
<jdbc:script location="classpath:com/bank/config/sql/schema.sql"/>
<jdbc:script location="classpath:com/bank/config/sql/test-data.sql"/>
</jdbc:embedded-database>
</beans>
<beans profile="production">
<jee:jndi-lookup id="dataSource" jndi-name="java:comp/env/jdbc/datasource"/>
</beans>
</beans>
Profile의 Annotation설정은 @Profile을 통해서 설정 가능하다. @Configuration과 함께 @Profile(“Profile명”)을 클래스에 쓰게 되면 내부 메소드에 붙은 @Bean을 통해 Bean들이 등록된다.
기존 @Configuration class에 대하여 살펴보고 @Profile을 통해 Bean을 설정하는 방법에 대하여 살펴본다.
Class위에 @Configuration을 붙이면 @Bean과 함께 정의한 메소드 명이 bean id로 등록된다.
@Configuration
public class TransferServiceConfig {
@Bean
public TransferService transferService() {
return new DefaultTransferService(accountRepository(), feePolicy());
}
@Bean
public AccountRepository accountRepository() {
return new JdbcAccountRepository(dataSource());
}
@Bean
public FeePolicy feePolicy() {
return new ZeroFeePolicy();
}
@Bean
public DataSource dataSource() {
return new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.HSQL)
.addScript("classpath:com/bank/config/sql/schema.sql")
.addScript("classpath:com/bank/config/sql/test-data.sql")
.build();
}
}
위에서 살펴봤던 Bean Profile XML 설정을 Annotation(@Profile)으로 설정하면 다음과 같다. @Profile은 XML의 beans profile의 설정과 똑같이 동작하며 @Bean은 XML의 bean설정과 매칭된다.
다음은 “dev” Profile을 정의했을 때의 Java코드이다. Profile명이 dev인 “dataSource” Bean을 정의하고 있다.
@Configuration
@Profile("dev")
public class StandaloneDataConfig {
@Bean
public DataSource dataSource() {
return new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.HSQL)
.addScript("classpath:com/bank/config/sql/schema.sql")
.addScript("classpath:com/bank/config/sql/test-data.sql")
.build();
}
}
다음은 “production” Profile을 정의했을 때의 Java코드이다. Profile명이 production인 “dataSource” Bean을 정의하고 있다.
@Configuration
@Profile("production")
public class JndiDataConfig {
@Bean
public DataSource dataSource() throws Exception {
Context ctx = new InitialContext();
return (DataSource) ctx.lookup("java:comp/env/jdbc/datasource");
}
}
설정한 Profile을 활성화 하는 방법에는 선언적인 Profile활성화, Java코드를 통한 Profile활성화가 있다.
web.xml 또는 JVM실행 시 환경변수, Property값 등으로 Profile을 활성화시킬 수 있다. Java코드를 통해서도 가능하나 실행환경이 war파일로 제공되거나 배포시점의 경우에는 관리의 불편함이 있으므로 web.xml을 통한 Profile활성화를 추천한다.
<web.xml로 Profile활성화>
<servlet>
<servlet-name>dispatcher</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>spring.profiles.active</param-name>
<param-value>production</param-value>
</init-param>
</servlet>
<JVM실행 시 환경변수로 Profile활성화>
java -Dspring.profiles.active="production"
Spring 3.1부터 Environment라는 인터페이스를 통해서 해당프로파일을 활성화시킬 수 있다.
<XML로 profile을 설정한 경우, Java코드로 Profile활성화> Profile명이 dev로 설정되어있는 bean을 활성화시킨다. GenericXmlApplicationContext에서 가져온 Environment를 통해 setActiveProfiles메소드로 Profile을 활성화시키고 해당 configuration xml을 로딩한다.
다음 예시에서는 Profile명이 production로 설정되어있는 bean이 활성화되며 Profile명이 dev으로 설정되어있는 bean은 Skip된다.
GenericXmlApplicationContext ctx = new GenericXmlApplicationContext();
ctx.getEnvironment().setActiveProfiles("production");
ctx.load("classpath:/com/bank/config/xml/*-config.xml");
ctx.refresh();
<@Profile로 Profile을 설정한 경우, Java코드로 Profile활성화 > 활성화하고자하는 Profile명을 setActiveProfiles메소드로 활성화시키고 “com.bank.config.code"패키지 내의 모든 @Configuration class를 스캔한다. Profile이 dev로 설정되어있는 bean이 활성화되며 Profile이 Production으로 설정되어있는 bean은 Skip된다.
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.getEnvironment().setActiveProfiles("dev");
ctx.scan("com.bank.config.code"); // find and register all @Configuration classes within
ctx.refresh();
JUnit4에서 Test class에 @ActiveProfile을 붙임으로써 Profile활성화가 가능하다. @ContextConfiguration과 함께 쓰며, @ActiveProfile뒤에 Profile명을 붙이면 해당 Profile이 활성화된다.
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(loader=AnnotationConfigContextLoader.class)
@ActiveProfiles("annotationProfile")
public class SpringAnnotationProfileTest {
}
Bean 정의는 많은 양의 설정 정보를 포함하고 있다. 자식 bean 정의는 부모 bean 정의로부터 설정 정보를 상속받은 bean 정의를 의미한다. 자식 bean 정의는 필요에 따라 부모 bean 정의로부터 상속받은 설정 정보를 덮어쓰거나 추가할 수 있다.
XML 기반 설정에서는 자식 bean 정의에 ‘parent’ attribute를 사용하여 상속관계를 정의할 수 있다.
<bean id="inheritedTestBean" abstract="true"
class="org.springframework.beans.TestBean">
<property name="name" value="parent"/>
<property name="age" value="1"/>
</bean>
<bean id="inheritsWithDifferentClass"
class="org.springframework.beans.DerivedTestBean"
parent="inheritedTestBean" init-method="initialize">
<property name="name" value="override"/>
<!-- the age property value of 1 will be inherited from parent -->
</bean>
자식 bean 정의는 bean class가 명기되어 있지 않을 경우, 부모 bean 정의의 값을 사용한다. 만약 자식 bean 정의에 bean class가 명기되어 있는 경우, 자식 bean 정의의 bean class는 부모 bean 정의의 모든 property 값을 받아들일 수 있어야 한다.
자식 bean 정의는 부모 bean 정의의 생성자 argument 값, property 값, 그리고 메소드 덮어씀을 상속받는다. 만약 init-method, destroy-method, static factory 메소드 설정이 명기되어 있을 경우, 부모의 설정을 덮어쓴다.
다음 설정은 항상 자식 bean 정의의 값을 따른다: depends on, autowire mode, dependency check, singleton, scope, lazy init.
부모 bean 정의는 abstract attribute를 사용하여 abstract로 설정할 수 있다. 이 경우, 부모 bean 정의는 class를 지정하지 않는다.
<bean id="inheritedTestBeanWithoutClass" abstract="true">
<property name="name" value="parent"/>
<property name="age" value="1"/>
</bean>
<bean id="inheritsWithClass" class="org.springframework.beans.DerivedTestBean"
parent="inheritedTestBeanWithoutClass" init-method="initialize">
<property name="name" value="override"/>
<!-- age will inherit the value of 1 from the parent bean definition-->
</bean>
부모 bean 정의는 완전하지 않기 때문에 객체화 될 수 없다.
Spring Framework의 IoC 컴포넌트는 확장을 고려하여 설계되었다. 일반적으로 어플리케이션 개발자가 다양한 BeanFactory 또는 ApplicationContext 구현 클래스를 상속받을 필요는 없다.
Spring IoC Container는 특별한 통합 interface의 구현체를 삽입하여 확장할 수 있다.
BeanPostProcessors interface는 다수의 callback 메소드를 정의하고 있는데, 어플리케이션 개발자는 이들 메소드를 구현함으로써 자신만의 객체화 로직(instantiation logic), 종속성 해결 로직(dependency-resolution logic) 등을 제공할 수 있다.
org.springframework.beans.factory.config.BeanPostProcessor interface는 두개의 callback 메소드로 구성되어 있다. 특정 class가 Container에 post-processor로 등록되면, post-processor는 Container에서 생성되는 각각의 bean 객체에 대해서, Container 객체화 메소드 전에 callback을 받는다.
중요한 것은 BeanFactory는 post-processor를 다루는 방식에 있어서 ApplicationContext와는 조금 다르다. ApplicationContext는 BeanPostProcessor interface를 구현한 bean을 자동적으로 인식하고 post-processor로 등록한다. 하지만 BeanFactory 구현을 사용하면 post-processor는 다음과 같이 명시적으로 등록되어야 한다.
ConfigurableBeanFactory factory = new XmlBeanFactory(...);
// now register any needed BeanPostProcessor instances
MyBeanPostProcessor postProcessor = new MyBeanPostProcessor();
factory.addBeanPostProcessor(postProcessor);
// now start using the factory
명시적인 등록은 불편하기 때문에 대부분의 Spring 기반 어플리케이션에서는 순수 BeanFactory 구현보다는 ApplicationContext 구현을 사용한다.
본 예제는 올바른 예는 아니지만, 기본적인 사용 방법을 보여주기 위함이다.
package scripting;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.beans.BeansException;
public class InstantiationTracingBeanPostProcessor implements BeanPostProcessor {
// simply return the instantiated bean as-is
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
return bean; // we could potentially return any object reference here...
}
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
System.out.println("Bean '" + beanName + "' created : " + bean.toString());
return bean;
}
}
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:lang="http://www.springframework.org/schema/lang"
xsi:schemaLocation=" http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/lang
http://www.springframework.org/schema/lang/spring-lang.xsd">
<lang:groovy id="messenger" script-source="classpath:org/springframework/scripting/groovy/Messenger.groovy">
<lang:property name="message" value="Fiona Apple Is Just So Dreamy."/>
</lang:groovy>
<!--
when the above bean ('messenger') is instantiated, this custom
BeanPostProcessor implementation will output the fact to the system console
-->
<bean class="scripting.InstantiationTracingBeanPostProcessor"/>
</beans>
InstantiationTracingBeanPostProcessor는 단순히 정의된다. 비록 이름을 가지고 있지는 않지만 bean이기 때문에 다른 bean과 같이 종속성은 삽입될 수 있다.
org.springframework.beans.factory.config.BeanFactoryPostProcessor는 BeanPostProcessor와 의미적으로 비슷하지만, 큰 차이점 중 하나는 BeanFactoryPostProcessors는 bean 설정 메타정보를 처리한다는 것이다. Spring IoC Container는 BeanFactoryPostProcessors가 설정 메타정보를 읽고, Container가 실제로 bean을 객체화 하기 전에 그 정보를 변경할 수 있도록 허용한다.
Bean factory post-processor는 BeanFactory의 경우 수동으로 실행되고, ApplicationContext의 경우 자동으로 실행된다.
BeanFactory에서는 다음과 같이 수동으로 실행한다.
XmlBeanFactory factory = new XmlBeanFactory(new FileSystemResource("beans.xml"));
// bring in some property values from a Properties file
PropertyPlaceholderConfigurer cfg = new PropertyPlaceholderConfigurer();
cfg.setLocation(new FileSystemResource("jdbc.properties"));
// now actually do the replacement
cfg.postProcessBeanFactory(factory);
PropertyPlaceholderConfigurer는 BeanFactory 정의로부터 property 값을 분리하기 위해 사용한다. 분리된 값은 Java Properties 형식으로 작성된 다른 파일로 분리된다. 이 방식은 주 XML 설정 파일을 변경하지 않고, 환경 변수 등을 변경할 때 유용하다.(예를 들어 database URLs, 사용자명, 패스워드 등)
<bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
<property name="locations">
<value>classpath:com/foo/jdbc.properties</value>
</property>
</bean>
<bean id="dataSource" destroy-method="close" class="org.apache.commons.dbcp.BasicDataSource">
<property name="driverClassName" value="${jdbc.driverClassName}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</bean>
위 설정의 실제 값은 아래와 같다.
jdbc.driverClassName=org.hsqldb.jdbcDriver
jdbc.url=jdbc:hsqldb:hsql://production:9002
jdbc.username=sa
jdbc.password=root
만약 Spring 2.5부터 지원되는 context namespace를 사용하면 다음과 같이 설정할 수 있다.
<context:property-placeholder location="classpath:com/foo/jdbc.properties"/>
PropertyPlaceholderConfigurer는 사용자가 지정한 Properties 파일 뿐 아니라 만약 지정한 property가 없을 경우, Java System properties도 검사할 수 있다. 이 기능은 systemPropertiesMode 설정을 통해 조절할 수 있다.
PropertyOverrideConfigurer는 또다른 bean factory post-processor로 PropertyPlaceholderConfigurer와 비슷하다. 하지만 PropertyPlaceholderConfigurer와는 반대로 원본 설정은 bean properties로 기본(default) 값을 가지거나 전혀 값을 가지지 않을 수 있다. 만약 Properties 파일이 특정 bean property를 위한 값을 가지고 있지 않을 경우, 기본 context definition이 사용된다. Properties 파일 설정의 각 줄은 다음과 같은 형식이다.
beanName.property=value
Spring 2.5부터 지원되는 context namespace를 사용하면 다음과 같이 설정할 수 있다.
<context:property-override location="classpath:override.properties"/>
org.springframework.beans.factory.FactoryBean interface를 구현한 객체는 스스로 fatory가 된다. FactoryBean interface는 Spring IoC Container에 객체화 로직을 삽입할 수 있는 방법이다. 만약 복잡한 객체화 코드를 가지고 있어 장황한 XML 설정보다는 Java로 직접 표현하는 것이 더 좋은 경우, 객체화 코드를 가지고 있는 FactoryBean를 생성하여 Container에서 삽입할 수 있다.
FactoryBean interface는 다음 3가지 메소드를 제공한다.
Object getObject(): 생성한 객체를 return한다. 생성된 객체는 공유될 수도 있다.
boolean isSingleton(): 만약 FactoryBean이 singleton을 리턴하면 true, 아니면 false 이다.
Class getObjectType(): getObject() 메소드에 의해 객체의 타입을 리턴하는데 미리 알 수 없는 경우에는 null을 리턴한다.
Container에게 FactoryBean이 생성한 객체가 아닌 FactoryBean 그 자체를 요구하는 경우도 있다. 이런 경우, BeanFactory의 getBean 메소드를 호출할 때 bean id 앞에 ‘&‘를 붙이면 된다.
org.springframework.context 패키지는 ApplicationContext를 통해 BeanFactory를 확장하여 애플리케이션 프레임워크에 맞는 추가 기능을 제공하며, 대부분의 경우 ContextLoader 등으로 자동 인스턴스화된다.org.springframework.context 패키지는 BeanFactory 인터페이스를 확장하는 ApplicationContext 인터페이스를 추가하고, 다른 인터페이스를 확장하여 보다 애플리케이션 프레임워크 지향적인 스타일로 추가 기능을 제공한다.
많은 사람들이 ApplicationContext를 완전히 선언적인 방식으로 사용하며, 프로그래밍 방식으로 생성하지 않고 ContextLoader와 같은 지원 클래스에 의존하여 Java EE 웹 애플리케이션의 정상적인 시작 프로세스의 일부로 ApplicationContext를 자동으로 인스턴스화한다.
ApplicationContext는 BeanFactory를 확장한 것으로 BeanFactory의 기능 외에 아래와 같은 기능을 제공한다.
아주 특별한 이유가 없는 한 ApplicationContext를 사용하는 것이 좋다. 다음은 BeanFactory와 ApplicationContext의 기능 비교표이다.
| Feature | BeanFactory | ApplicationContext |
|---|---|---|
| Bean 객체화/엮음 | Yes | Yes |
| BeanPostProcessor 자동 등록 | No | Yes |
| BeanFactoryPostProcessor 자동 등록 | No | Yes |
| 편리한 MessageSource 접근(for i18n) | No | Yes |
| ApplicationEvent 발송 | No | Yes |
본 장의 내용은 Resource를 참조한다.
ApplicationContext는 Event 처리를 위해 ApplicationEvent, ApplicationListener interface를 제공한다. ApplicationListener interface를 구현한 bean은 ApplicationContext에 발생한 모든 ApplicationEvent를 전달받는다. Spring이 제공하는 표준 event는 다음과 같다.
| Event | 설명 |
|---|---|
| ContextRefreshedEvent | ApplicationContext가 초기화된거나 refresh될 때 발생한다(refresh하기 위해서 ConfigurableApplicationContext interface의 refresh() 메소드를 사용한다). “초기화”라는 단어는, 모든 bean이 load되었고, post-processor bean이 탐지되어 활성화되었으며, singleton 객체가 선객체화되어, ApplicationContext 객체가 사용가능한 상태에 있다는 것을 의미한다. refresh는 context가 닫혀지지 않은 한, 여러번 발생할 수 있으며, ApplicationContext가 “hot” refresh를 지원해야한다 (XmlWebApplicationContext는 “hot” refresh를 지원하지만, GenericApplicationContext는 지원하지 않는다). |
| ContextStartedEvent | ApplicationContext가 시작될 때 발생한다(시작하기 위해서 ConfigurableApplicationContext interface의 start() 메소드를 사용한다). “시작됨(Started)“이란 단어는, 모든 Lifecycle bean이 명시적인 시작 신호를 받았음을 의미한다. 이 event는 일반적으로 명시적인 정지(stop) 후에, 재시작하기 위해서 사용되지만, 자동시작(autostart)로 설정되지 않은 컴포넌트를 시작하기 위해서도 사용된다 |
| ContextStoppedEvent | ApplicationContext가 정지할 때 발생한다(정지하기 위해서 ConfigurableApplicationContext interface의 stop() 메소드를 사용한다). “정지됨(Stopped)“란 단어는, 모든 Lifecycle bean이 명시적인 정지 신호를 받았음을 의미한다. 정지된 context는 start() 호출을 통해 재시작될 수 있다. |
| ContextClosedEvent | ApplicationContext가 닫혔을 때 발생한다(닫기 위해서 ConfigurableApplicationContext interface의 close() 메소드를 사용한다). “닫힘(Closed)“란 단어는, 모든 singleton bean이 파괴되었음을 의미한다. 닫힌 context는 생명주기의 끝에 도달한 것으로, refresh 되거나 재시작될 수 없다. |
| RequestHandledEvent | 웹에 특화된 event로서, HTTP request가 처리되었음을 알린다(request가 종료된 후에 발송된다). Spring의 DispatcherServlet를 사용하는 웹 어플리케이션인 경우에만 사용할 수 있다. |
새로운 event를 구현하는 것도 어렵지 않다. ApplicationContext interface를 구현한 새로운 event 객체를 ApplicationContext의 publishEvent() 메소드를 통해 발행하면 된다. publishEvent() 메소드는 모든 listener가 event 처리를 마칠때까지 block 상태로 있게 된다. 게다가 transaction context가 가능하다면, listener가 event를 받았을 때, 발행모듈의 transaction context 내에서 event를 처리한다.
아래는 예제이다.
<bean id="emailer" class="example.EmailBean">
<property name="blackList">
<list>
<value>black@list.org</value>
<value>white@list.org</value>
<value>john@doe.org</value>
</list>
</property>
</bean>
<bean id="blackListListener" class="example.BlackListNotifier">
<property name="notificationAddress" value="spam@list.org"/>
</bean>
public class EmailBean implements ApplicationContextAware {
private List blackList;
private ApplicationContext ctx;
public void setBlackList(List blackList) {
this.blackList = blackList;
}
public void setApplicationContext(ApplicationContext ctx) {
this.ctx = ctx;
}
public void sendEmail(String address, String text) {
if (blackList.contains(address)) {
BlackListEvent event = new BlackListEvent(address, text);
ctx.publishEvent(event);
return;
}
// send email...
}
}
public class BlackListNotifier implements ApplicationListener {
private String notificationAddress;
public void setNotificationAddress(String notificationAddress) {
this.notificationAddress = notificationAddress;
}
public void onApplicationEvent(ApplicationEvent event) {
if (event instanceof BlackListEvent) {
// notify appropriate person...
}
}
}
BeanFactory가 프로그램적으로 생성되는 것과 반대로, ApplicationContext 객체는 ContextLoader 등을 사용하여 선언적으로 생성될 수 있다. 물론 ApplicationContext 객체 역시 프로그램적으로 생성할 수 있다.
ContextLoader에는 ContextLoaderListener와 ContextLoaderServlet가 있다. 둘 다 기능적으로는 같지만, listener 버전은 Servlet 2.3 컨테이너에서는 사용할 수 있다.
Servlet 2.4 스팩 이후로, servlet context listener는 웹 어플리케이션을 위한 servlet context가 생성되어 첫번째 요청을 처리할 상태가 된 직후 수행된다(그리고 servlet context가 막 종료되었을 때도 수행된다).
따라서 servlet context listner가 Spring ApplicationContext를 초기화할 최적의 장소이다.
ContextLoaderListener를 사용하여 ApplicationContext를 등록하는 방법은 아래와 같다.
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/daoContext.xml /WEB-INF/applicationContext.xml</param-value>
</context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<!-- or use the ContextLoaderServlet instead of the above listener
<servlet>
<servlet-name>context</servlet-name>
<servlet-class>org.springframework.web.context.ContextLoaderServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
-->
Listener는 ‘contextConfigLocation’ 파라미터를 검사한다. 만약 존재하지 않으면 기본적으로 /WEB-INF/applicationContext.xml를 사용할 것이다.
만약 ‘contextConfigLocation’ 파라미터 값이 존재할 경우, 미리 정해놓은 구분자(comma(’,’), semicolon(’;’), 공백문자(whitespace))를 사용하여 파라미터 문자열을 분리한 후, application context를 찾을 것이다.
Ant-style path 패턴이 지원된다. 예를 들어 /WEB-INF/*Context.xml’은 “WEB-INF” 디렉토리에 존재하는 “Context.xml”로 끝나는 이름을 가진 모든 파일을 의미하고, \
/WEB-INF/**/*Context.xml은 “WEB-INF” 디렉토리 및 하위 디렉토리에 존재하는 “Context.xml”로 끝나는 이름을 가진 모든 파일을 의미한다.
Spring Framework는 Spring의 종속성 삽입을 위해 annotation을 사용할 수 있다. Spring 2.0에서는 @Required 어노테이션으로 필수 속성을 강제할 수 있는 기능이 도입되었고 Spring 2.5에서는 이와 동일한 일반적인 접근 방식을 따라 Spring의 의존성 주입을 구동할 수 있게 되었으며, Spring 3.0부터 @Inject 및 @Named와 같이 javax.inject 패키지에 포함된 JSR-330(Java용 의존성 주입) 어노테이션에 대한 지원이 추가되었다.
Spring @Autowired annotation은 자동 엮음과 같은 기능을 제공하지만, 좀 더 세밀한 제어와 넓은 사용성을 제공한다. Spring Framework는 @Resource, @PostConstruct, @PreDestroy 등의 JSR-250 annotation도 지원한다. 이들 annotation을 사용하기 위해서는 Spring Container에 특정 BeanPostProcessors를 등록해야만 한다. 항상 그렇듯이, 이들 BeanPostProcessors가 개별적인 bean 정의로 등록될 수도 있지만, ‘context’ namespace를 사용하여 등록할 수도 있다.
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<context:annotation-config/>
</beans>
(위 <context:annotation-config/> tag를 사용하면, AutowiredAnnotationBeanPostProcessor, CommonAnnotationBeanPostProcessor, PersistenceAnnotationBeanPostProcessor, RequiredAnnotationBeanPostProcessor를 등록해 준다.)
@Required annotation은 bean property setter 메소드에 적용된다.
public class SimpleMovieLister {
private MovieFinder movieFinder;
@Required
public void setMovieFinder(MovieFinder movieFinder) {
this.movieFinder = movieFinder;
}
// ...
}
이 annotation은 단순히 bean property가 설정 시 반드시 설정되어야만 한다는 것을 나타낸다. 즉, bean 정의에 명시적으로 property 값을 선언하거나 자동 엮임을 통해서 설정되어야만 한다는 것을 의미한다. 만약 annotation이 적용된 bean property에 대한 설정이 이루어지지 않는 경우, Container는 exception을 던진다.
@Autowired annotation은 “전통적인” setter 메소드에 적용된다.
public class SimpleMovieLister {
private MovieFinder movieFinder;
@Autowired
public void setMovieFinder(MovieFinder movieFinder) {
this.movieFinder = movieFinder;
}
// ...
}
뿐만 아니라, 임의의 이름과 다수의 argument를 가진 메소드에도 적용될 수 있다.
public class MovieRecommender {
private MovieCatalog movieCatalog;
private CustomerPreferenceDao customerPreferenceDao;
@Autowired
public void prepare(MovieCatalog movieCatalog, CustomerPreferenceDao customerPreferenceDao) {
this.movieCatalog = movieCatalog;
this.customerPreferenceDao = customerPreferenceDao;
}
// ...
}
또한, @Autowired annotation은 생성자 및 field에도 적용될 수 있다.
public class MovieRecommender {
@Autowired
private MovieCatalog movieCatalog;
private CustomerPreferenceDao customerPreferenceDao;
@Autowired
public MovieRecommender(CustomerPreferenceDao customerPreferenceDao) {
this.customerPreferenceDao = customerPreferenceDao;
}
// ...
}
또한, array 타입의 field나 메소드에 적용함으로써, ApplicationContext에 존재하는 특정 Type의 모든 bean을 field나 메소드에 제공하는 것도 가능한다.
public class MovieRecommender {
@Autowired
private MovieCatalog[] movieCatalogs;
// ...
}
Typed collections에도 같은 방식이 적용된다.
public class MovieRecommender {
private Set<MovieCatalog> movieCatalogs;
@Autowired
public void setMovieCatalogs(Set<MovieCatalog> movieCatalogs) {
this.movieCatalogs = movieCatalogs;
}
// ...
}
심지어 typed Map 역시 key 타입이 String인 한 자동 엮임이 가능하다. Map은 기대한 타입의 모든 bean을 value로 갖게 되고, key는 해당하는 bean의 이름이 된다.
public class MovieRecommender {
private Map<String, MovieCatalog> movieCatalogs;
@Autowired
public void setMovieCatalogs(Map<String, MovieCatalog> movieCatalogs) {
this.movieCatalogs = movieCatalogs;
}
// ...
}
기본적으로, 자동 엮임은 대상이 되는 bean이 없을 경우 실패한다. 기본적으로 annotation이 적용된 메소드, 생성자, field는 필수로 간주한다. 아래와 같이 설정하여 기본 행동 방식을 변경할 수 있다.
public class SimpleMovieLister {
private MovieFinder movieFinder;
@Autowired(required=false)
public void setMovieFinder(MovieFinder movieFinder) {
this.movieFinder = movieFinder;
}
// ...
}
@Autowired annotation은 잘 알려진 “분석가능한 종속성(resolvable dependencies)“에도 사용될 수 있다 : BeanFactory interface, ApplicationContext interface, ResourceLoader interface, ApplicationEventPublisher interface, MessageSource interface(그리고 이들을 상속한 ConfigurableApplicationContext 또는 ResourcePatternResolver interface)는 특별한 설정 없이 자동적으로 해결(resolve)된다.
Type을 이용한 자동 엮기는 대상이 다수가 발생할 수 있기 때문에, 선택 시 추가적인 제어가 필요하다. 한 방법으로 Spring의 @Qualifier annotation을 사용할 수 있다. 특정 argument를 qualifier와 관련시킴으로써, 타입을 찾을 대상을 좁히고, 각 argument에 해당하는 대상 bean을 선택할 수 있다.
public class MovieRecommender {
@Autowired
@Qualifier("main")
private MovieCatalog movieCatalog;
// ...
}
@Qualifier annotation은 생성자의 argument 및 메소드의 parameter 각각에 적용할 수 있다.
public class MovieRecommender {
private MovieCatalog movieCatalog;
private CustomerPreferenceDao customerPreferenceDao;
@Autowired
public void prepare(@Qualifier("main") MovieCatalog movieCatalog, CustomerPreferenceDao customerPreferenceDao) {
this.movieCatalog = movieCatalog;
this.customerPreferenceDao = customerPreferenceDao;
}
// ...
}
일치하는 bean 정의는 아래 예제에서 찾을 수 있다. Qualifier “main” 값을 가진 bean이 같은 값의 @Qualifier annotation이 있는 생성자 argument로 엮인다.
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<context:annotation-config/>
<bean class="example.SimpleMovieCatalog">
<qualifier value="main"/>
<!-- inject any dependencies required by this bean -->
</bean>
<bean class="example.SimpleMovieCatalog">
<qualifier value="action"/>
<!-- inject any dependencies required by this bean -->
</bean>
<bean id="movieRecommender" class="example.MovieRecommender"/>
</beans>
대체 수단으로, bean 이름을 qualifier로 간주된다. 즉, qualifier element 대신 bean id가 “main”이라면 동일하게 동작한다. 어쨌든, 이름으로 bean을 찾는게 더 좋지만, @Autowired는 기본적으로 type을 기반으로 동작하고 qualifier는 선택적이다. 즉, 타입으로 bean 대상을 줄인 후에 qualifier 또는 bean name으로 대상을 좁힌다. Qualifier 값은 의미적으로 유일한 bean id를 나타내지는 않는다. 좋은 qualifier 값은 “main”, “EMEA”, “persistent” 등과 같이 bean id가 아닌 특정 컴포넌트의 특징을 표현하는 것이다. Qualifier는 typed collection에도 적용된다.
Spring은 field나 bean property setter 메소드에 적용된 JSR-250 @Resource annotation을 사용하여 종속성 삽입을 지원한다. @Resource는 ’name’ attribute를 가지고, Spring은 그 값을 삽입할 bean 이름으로 인식한다.
public class SimpleMovieLister {
private MovieFinder movieFinder;
@Resource(name="myMovieFinder")
public void setMovieFinder(MovieFinder movieFinder) {
this.movieFinder = movieFinder;
}
}
만약 name이 명식적으로 설정되어 있지 않으면, field나 setter 메소드의 이름으로부터 name 값을 유추해낸다. Field의 경우, field 명과 같다. Setter 메소드의 경우, bean property 이름과 같다. 아래 예제에서는 “movieFinder” bean이 삽입된다.
public class SimpleMovieLister {
private MovieFinder movieFinder;
@Resource
public void setMovieFinder(MovieFinder movieFinder) {
this.movieFinder = movieFinder;
}
}
@Autowired와 비슷하게, @Resource도 대안으로 bean type으로 대상을 찾는다. 뿐만 아니라 잘 알려진 “resolvable dependencies”로 해결한다. 둘 다 명시적으로 name을 설정하지 않은 경우 적용된다. 다음 예에서 customerPreferenceDao field를 위해서 “customerPreferenceDao” 이름을 가진 bean을 먼저 찾는다. 그 다음으로 CustomerPreferenceDao Type의 bean을 찾는다. “context” field는 알려진 해결가능한 종속성 type인 ApplicationContext에 기반하여 삽입된다.
public class MovieRecommender {
@Resource
private CustomerPreferenceDao customerPreferenceDao;
@Resource
private ApplicationContext context;
public MovieRecommender() {
}
// ...
}
CommonAnnotationBeanPostProcessor는 @Resource annotation 뿐 아니라 JSR-250 lifecycle annotation 역시 인식한다.
public class CachingMovieLister {
@PostConstruct
public void populateMovieCache() {
// populates the movie cache upon initialization...
}
@PreDestroy
public void clearMovieCache() {
// clears the movie cache upon destruction...
}
}
BeanDefinition을 생성하기 위한 설정 메타데이터로 XML을 사용했다. 이전 섹션에서는 소스 레벨의 어노테이션을 사용해 많은 설정 메타데이터를 제공할 수 있음을 보여주었다. 그러나 이 예제들에서도 기본적인 bean 정의는 여전히 XML 파일에 명시적으로 작성되었다. 이번 섹션에서는 classpath를 검색하고 filter를 사용해 대상 컴포넌트(candidate component) 를 검출하는 방법을 소개한다.본 장의 앞선 대부분의 예제들은 Spring Container 안에서 BeanDefinition을 생성하기 위한 설정 메타데이터를 명기하기 위해서 XML을 사용해왔다. 이전 section Annotaion-based configuration은 source-level annotation을 사용하여 많은 양의 설정 메타데이터를 제공할 수 있음을 보였다. 이들 예제에서도 어쨌든, “base” bean 정의가 XML 파일 안에 명시적으로 정의되었다. 이번 section은 classpath를 검색하고, filter를 통해 검사함으로써, 대상 컴퍼넌트(candidate component) 를 검출하는 방법을 소개한다.
Spring 2.0부터 Data Access Object(DAO) 등과 같은 repository를 표시하기 위해서 @Repository annotation이 소개되었다. Spring 2.5는 추가적으로 @Component, @Service, @Controller annotation을 제공한다. @Component는 Spring이 관리하는 컴포넌트를 위한 포괄적인 stereotype을 나타낸다. 그리고 @Repository, @Service, @Controller는 좀 더 특별한 사용을 위한 @Component의 일종이다. (각각 persistence, service, presentation layer의 component를 의미한다.)
Spring은 ‘stereotyped’ class를 자동으로 탐지하고 ApplicationContext에 일치하는 BeanDefinition을 등록하는 기능을 제공한다. 아래 두 class는 자동 탐지의 대상이 된다.
@Service
public class SimpleMovieLister {
private MovieFinder movieFinder;
@Autowired
public SimpleMovieLister(MovieFinder movieFinder) {
this.movieFinder = movieFinder;
}
}
@Repository
public class JpaMovieFinder implements MovieFinder {
// implementation elided for clarity
}
실제로 위 두 class를 자동 탐지하고 상응하는 bean을 등록하기 위해서는, 아래 예제 XML의 <context:component-scan/> element의 ‘basePackage’가 위 두 class의 공통 부모 package이어야 한다(또는 comma(’,’)로 구분된 list 역시 가능하다).
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<context:component-scan base-package="org.example"/>
</beans>
검색 과정에서 component가 자동 탐지되었을 때, bean 이름은 scan하고 있는 BeanNameGenerator 전략에 따라 생성된다. 기본적으로 name 값을 가지고 있는 Spring ‘stereotype’ annotation(@Component, @Repository, Service, Controller)는 상응하는 bean 정의에게 이름을 제공한다. 만약 이들 annotation이 name이 없거나 또 다른 탐지된 component인 경우, 기본 bean name generator는 class 이름의 첫 문자를 소문자로 변환한 값을 return할 것이다. 예를 들어, 아래 예제에서 두개의 component가 탐지되는데 각각의 이름은 ‘myMovieLister’와 ‘movieFinderImpl’이다.
@Service("myMovieLister")
public class SimpleMovieLister {
// ...
}
@Repository
public class MovieFinderImpl implements MovieFinder {
// ...
}
일반적으로 Spring 관리 component는 ‘singleton’이다. 어쨌든 다른 scope이 필요한 경우가 있다. Spring Framework는 @Scope annotation을 제공한다.
@Scope("prototype")
@Repository
public class MovieFinderImpl implements MovieFinder {
// ...
}
이번 section에서는 자동 엮임의 대상을 찾을 때 상세한 제어를 제공하기 위해 @Qualifier annotation을 사용하는 방법을 설명한다.
@Component
@Qualifier("Action")
public class ActionMovieCatalog implements MovieCatalog {
// ...
}
스프링 3.0부터 JSR-330 표준 어노테이션(의존성 주입)을 지원한다. 이 어노테이션들은 스프링 어노테이션들과 같은 방법으로 스캔된다. 이 어노테이션들을 사용하기 위해서는 클래스패스에 관련 jar 파일들을 가지고 있어야 한다.
Maven을 사용한다면 Maven Repository(https://mvnrepository.com/artifact/javax.inject/javax.inject/1)에서 javax.inject라는 아티펙트가 제공된다. pom.xml 파일에 아래의 의존성을 추가하여 사용할 수 있다.
<dependency>
<groupId>javax.inject</groupId>
<artifactId>javax.inject</artifactId>
<version>1</version>
</dependency>
@Autowired를 대신하여 @javax.inject.Inject를 아래와 같이 사용할 수 있다.
import javax.inject.Inject;
public class SimpleMovieLister {
private MovieFinder movieFinder;
@Inject
public void setMovieFinder(MovieFinder movieFinder) {
this.movieFinder = movieFinder;
}
public void listMovies() {
this.movieFinder.findMovies(...);
// ...
}
}
@Autowired와 같이 @Inject를 필드 수준, 함수 수준, 생성자 인자 수준으로 사용할 수 있고, 주입 지점을 Provider로 선언할 수 있으며 Provider.get() 호출을 통해 근접 범위의 Bean들에 대한 요청 시 접근 또는 다른 Bean에 대한 지연된 접근을 허용할 수 있다.
import javax.inject.Inject;
import javax.inject.Provider;
public class SimpleMovieLister {
private Provider<MovieFinder> movieFinder;
@Inject
public void setMovieFinder(Provider<MovieFinder> movieFinder) {
this.movieFinder = movieFinder;
}
public void listMovies() {
this.movieFinder.get().findMovies(...);
// ...
}
}
주입될 의존성에 대해 지정된 이름을 사용하고자 할 경우에는 아래와 같이 @Named 어노테이션을 사용할 수 있다.
import javax.inject.Inject;
import javax.inject.Named;
public class SimpleMovieLister {
private MovieFinder movieFinder;
@Inject
public void setMovieFinder(@Named("main") MovieFinder movieFinder) {
this.movieFinder = movieFinder;
}
// ...
}
@Component를 대신하여, @javax.inject.Named나 javax.annotation.ManagedBean를 아래와 같이 사용할 수 있다.
import javax.inject.Inject;
import javax.inject.Named;
@Named("movieListener") // @ManagedBean("movieListener") could be used as well
public class SimpleMovieLister {
private MovieFinder movieFinder;
@Inject
public void setMovieFinder(MovieFinder movieFinder) {
this.movieFinder = movieFinder;
}
// ...
}
일반적으로 컴포넌트에 대한 이름을 명시하지 않고 @Component를 사용할 수 있는데, 아래 예제처럼 @Named도 비슷하게 사용할 수 있다.
import javax.inject.Inject;
import javax.inject.Named;
@Named
public class SimpleMovieLister {
private MovieFinder movieFinder;
@Inject
public void setMovieFinder(MovieFinder movieFinder) {
this.movieFinder = movieFinder;
}
// ...
}
@Named나 @ManagedBean을 사용할 때 아래 예제에서 보여주는 것처럼 스프링 어노테이션과 같이 동일한 방법으로 컴포넌트 탐색이 가능하다.
@Configuration
@ComponentScan(basePackages = "org.example")
public class AppConfig {
// ...
}
표준 어노테이션으로 작업할 때 아래 표와 같이 일부 중요한 기능이 사용 불가능하다.
| Spring | javax.inject.* | javax.inject 제한 및 비고 |
|---|---|---|
| @Autowired | @Inject | @Inject는 required 속성이 없다. 자바 8의 Optional을 대신 사용할 수 있다. |
| @Component | @Named / @ManagedBean | JSR-330은 조합구성을 제공하지 않기 때문에 명명된 컴포넌트만 식별해야 한다. |
| @Scope(“singleton”) | @Singleton | JSR-330의 기본범위는 스프링의 prototype 같으나 스프링의 일반 기본값과 일관성을 유지하기 위해 스프링 컨테이너에 선언된 JSR-330 빈은 기본적으로 singleton이다. Singleton이 아닌 다른 범위를 사용하려면 스프링의 @Scope 어노테이션을 사용해야 한다. javax.inject도 @Scope 어노테이션을 제공하지만 자체적 어노테이션을 생성할 때 사용한다. |
| @Qualifier | @Qualifier / @Named | javax.inject.Qualifier는 사용자 정의 한정자를 만들기 위한 메타 어노테이션으로 스프링의 값이 있는 @Qualifier 같은 구체적인 String으로 된 한정자는 javax.inject.Named로 연결할 수 있습니다. |
| @Value | - | 동등한 것이 없음 |
| @Required | - | 동등한 것이 없음 |
| @Lazy | - | 동등한 것이 없음 |
| ObjectFactory | Provider | javax.inject.Provider는 짧은 get() 함수명만 있는 스프링의 ObjectFactory의 직접적 대안으로 사용할 수 있으며 스프링의 @Autowired, 어노테이션이 없는 생성자나 Setter 함수와 조합하여 사용될 수 있다. |
Java 코드에서 주석을 사용하여 스프링 컨테이너를 구성하는 방법에 대해 알아본다.
스프링의 자바 기반 설정에서는 @Configuration 어노테이션 클래스와 @Bean 어노테이션 메소드를 지원한다.
@Bean 어노테이션은 Spring IoC 컨테이너가 관리할 새로운 객체를 인스턴스화하고, 초기화하는데 사용되며, Spring의 XML 설정에서의 <bean/>과 같은 역할을 한다.
@Bean 어노테이션은 붙인 메소드는 스프링 @Component와 함께 사용할 수 있지만, 대체로 @Configuration Bean과 사용한다.
@Configuration 어노테이션은 해당 클래스의 목적이 Bean 설정을 위한 소스임을 나타내며, @Configuration 클래스는 같은 클래스 안에 있는 @Bean 메소드들끼리 서로를 호출하여 Bean 사이의 의존성을 정의할 수 있게 한다.
@Configuration 클래스를 아래와 같이 구성할 수 있다.
@Configuration
public class AppConfig {
@Bean
public MyService myService() {
return new MyServiceImpl();
}
}
예제의 AppConfig 클래스는 아래의 Spring <bean/> XML과 동일한 역할을 한다.
<beans>
<bean id="myService" class="com.acme.services.MyServiceImpl"/>
</beans>
AnnotationConfigApplicationContext는 다재다능한 ApplicationContext 구현체로, 입력으로 @Configuration 클래스뿐만 아니라 평범한 @Component 클래스와 JSR-330 메타데이터로 어노테이션이 붙은 클래스들도 받아들일 수 있다.
@Configuration 클래스가 입력으로 제공되면 @Configuration 클래스 자체가 Bean 정의로 등록되고, 해당 클래스내의 선언된 모든 @Bean 메서드들도 Bean 정의로 등록된다.
@Component와 JSR-330 클래스들이 제공되면 이 클래스들은 Bean 정의로 등록되고 해당 클래스내에서 필요한 곳에 @Autowired나 @Inject 같은 DI 메타데이터가 사용되었다고 가정한다.
ClassPathXmlApplicationContext를 인스턴스화 할 때 스프링 XML 파일을 사용하는 방법과 거의 동일하게 AnnotationConfigApplicationContext를 인스턴스화 할 때 @Configuration 클래스들을 입력으로 사용할 것이다.
이를 통해 전혀 XML을 사용하지 않고 스프링 컨테이너를 사용할 수 있다.
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class);
MyService myService = ctx.getBean(MyService.class);
myService.doStuff();
}
인수 없는 생성자를 사용하여 AnnotationConfigApplicationContext를 인스턴스화한 다음 register() 메서드를 사용하여 구성할 수 있다.
이 접근 방식은 AnnotationConfigApplicationContext를 프로그래밍 방식으로 빌드할 때 특히 유용하며 아래와 같이 사용할 수 있다.
public static void main(String[] args) {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.register(AppConfig.class, OtherConfig.class);
ctx.register(AdditionalConfig.class);
ctx.refresh();
MyService myService = ctx.getBean(MyService.class);
myService.doStuff();
}
@Configuration구성 요소 스캔을 활성화하려면 다음과 같이 구성할 수 있다.
@Configuration
@ComponentScan(basePackages = "com.acme") // 구성요소 스캔을 활성화함
public class AppConfig {
// ...
}
경험있는 스프링 사용자들은 다음과 같이 일반적으로 사용되는 스프링의 context: 네임스페이스로 XML을 선언하는데 익숙할 것이다.
<beans>
<context:component-scan base-package="com.acme"/>
</beans>
위의 예제에서 com.acme 팩키지는 스캔되고 @Component 어노테이션이 붙은 클래스들을 찾고 이러한 클래스를 컨테이너내 스프링 빈 정의로 등록할 것이다.
AnnotationConfigApplicationContext에는 같은 컴포넌트 스캔 기능을 하는 scan(String…) 메서드가 있다.
public static void main(String[] args) {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.scan("com.acme");
ctx.refresh();
MyService myService = ctx.getBean(MyService.class);
}
AnnotationConfigApplicationContext의 WebApplicationContext 변형은 AnnotationConfigWebApplicationContext로 사용할 수 있다.
이 구현체는 스프링 ContextLoaderListener 서블릿 리스너, 스프링 MVC DispatcherServlet 등을 설정할 때 사용할 수 있다.
아래는 전형적인 스프링 MVC 웹 어플리케이션을 설정하는 web.xml의 예제로 contextClass context-param과 init-param의 사용방법을 보여준다.
<web-app>
<!-- 기본 XmlWebApplicationContext 대신 AnnotationConfigWebApplicationContext를 사용하는 ContextLoaderListener를 설정한다. -->
<context-param>
<param-name>contextClass</param-name>
<param-value>
org.springframework.web.context.support.AnnotationConfigWebApplicationContext
</param-value>
</context-param>
<!-- 설정 위치는 반드시 콤마나 공백을 구분자로 사용하는 하나 이상의 정규화된 @Configuration 클래스들로 구성되어야 한다. 정규화된 패키지는 컴포넌트 스캔으로 지정될 수도 있다. -->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>com.acme.AppConfig</param-value>
</context-param>
<!-- ContextLoaderListener를 사용해서 루트 어플리케이션 시작한다. -->
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<!-- Spring MVC DispatcherServlet 시작 -->
<servlet>
<servlet-name>dispatcher</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<!-- 기본 XmlWebApplicationContext 대신 AnnotationConfigWebApplicationContext를 사용한 DispatcherServlet 설정한다. -->
<init-param>
<param-name>contextClass</param-name>
<param-value>
org.springframework.web.context.support.AnnotationConfigWebApplicationContext
</param-value>
</init-param>
<!-- 다시한번, 설정 위치는 반드시 콤마나 공백을 구분자로 사용하는 하나 이상의 정규화된 @Configuration 클래스들로 구성되어야 한다. -->
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>com.acme.web.MvcConfig</param-value>
</init-param>
</servlet>
<!-- /app/*에 대한 모든 요청을 디스패쳐 서블릿에 매핑한다. -->
<servlet-mapping>
<servlet-name>dispatcher</servlet-name>
<url-pattern>/app/*</url-pattern>
</servlet-mapping>
</web-app>
@Bean은 메소드 레벨 어노테이션이며 XML <bean/> 요소와 동일하며 <bean/>에서 제공하는 일부 속성을 지원한다. 또한, @Configuration 어노테이션 또는 @Component 어노테이션 클래스에서 @Bean 어노테이션을 사용할 수 있다.
메소드에 @Bean 어노테이션을 사용하면 Bean으로 선언된다. 이 메서드를 사용하여 메서드의 반환 값으로 지정된 유형의 ApplicationContext 내에 Bean 정의를 등록되며 Bean 이름은 메서드 이름과 동일하다.
다음 예제에서 @Bean 메소드 선언을 확인할 수 있다.
@Configuration
public class AppConfig {
@Bean
public TransferServiceImpl transferService() {
return new TransferServiceImpl();
}
}
위 예제는 다음 Spring XML과 정확히 동일하다.
<beans>
<bean id="transferService" class="com.acme.TransferServiceImpl"/>
</beans>
@Bean 어노테이션 메서드는 해당 Bean을 빌드하는 데 필요한 종속성을 설명하는 임의 개수의 매개변수를 가질 수 있다.
예를 들어 TransferService에 AccountRepository가 필요한 경우 다음 예제와 같이 메서드 매개 변수를 사용하여 해당 종속성을 구체화할 수 있다.
@Configuration
public class AppConfig {
@Bean
public TransferService transferService(AccountRepository accountRepository) {
return new TransferServiceImpl(accountRepository);
}
}
@Bean 어노테이션으로 정의된 Bean이 특정 범위를 갖도록 지정할 수 있으며, 선언된 Bean의 범위는 Bean scope에 지정된 표준 범위를 사용할 수 있다. 기본 범위는 싱글톤이지만 다음 예제와 같이 @Scope 어노테이션으로 이를 재정의할 수 있다.
@Configuration
public class MyConfiguration {
@Bean
@Scope("prototype")
public Encryptor encryptor() {
// ...
}
}
@Configuration은 객체가 Bean 정의의 소스임을 나타내는 클래스 수준 어노테이션이며 @Configuration 클래스는 @Bean 어노테이션 메서드를 통해 빈을 선언한다.
@Configuration 클래스의 @Bean 메소드에 대한 호출은 bean 간 종속성을 정의하는 데에도 사용할 수 있다.
@Configuration
public class AppConfig {
@Bean
public BeanOne beanOne() {
return new BeanOne(beanTwo());
}
@Bean
public BeanTwo beanTwo() {
return new BeanTwo();
}
}
Spring의 Java 기반 설정 기능을 사용하면 구성의 복잡성을 줄일 수 있는 어노테이션을 작성할 수 있다.
스프링 XML 파일에서 설정 모듈화에 <import/>요소를 사용하기는 하지만 @Import 어노테이션은 다른 설정 클래스에서 @Bean 설정을 로딩한다.
@Configuration
public class ConfigA {
@Bean
public A a() {
return new A();
}
}
@Configuration
@Import(ConfigA.class)
public class ConfigB {
@Bean
public B b() {
return new B();
}
}
이제 컨텍스트를 인스턴스화 할 때 ConfigA.class와 ConfigB.class를 둘 다 지정해야하는 대신 ConfigB만 명시적으로 제공하면 된다.
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(ConfigB.class);
// now both beans A and B will be available...
A a = ctx.getBean(A.class);
B b = ctx.getBean(B.class);
}
이 접근방식은 설정을 구성하는 동안 잠재적으로 많은 수의 @Configuration 클래스들을 기억해야 하는 대신 하나의 클래스만 다루면 되므로 컨테이너 인스턴스화를 단순화한다.
위의 예제는 작동하지만 단순하다. 대부분의 실제 시나리오에서 Bean은 다른 설정 클래스들에 대한 의존성을 가지게 된다.
XML을 사용할 때 컴파일러가 관여하지 않고 그냥 ref=“someBean”만 선언한 뒤 스프링이 컨테이너를 인스턴스화 하면 제대로 동작하기를 믿으면 되기 때문에 의존성 자체는 이슈가 아니었다.
@Configuration를 사용할 때 자바 컴파일러는 다른 빈에 대한 참조는 유효한 자바문법이어야 한다는 제약을 설정 모델에 둔다.
다행히도 이 문제를 해결하는 것은 간단하다. 이미 논의한 것처럼 @Bean 메소드는 Bean 종속성을 설명하는 임의의 수의 매개변수를 가질 수 있다.
각각 다른 클래스에서 선언된 Bean에 따라 여러 @Configuration 클래스가 있는 다음과 같이 구성할 수 있다.
@Configuration
public class ServiceConfig {
@Bean
public TransferService transferService(AccountRepository accountRepository) {
return new TransferServiceImpl(accountRepository);
}
}
@Configuration
public class RepositoryConfig {
@Bean
public AccountRepository accountRepository(DataSource dataSource) {
return new JdbcAccountRepository(dataSource);
}
}
@Configuration
@Import({ServiceConfig.class, RepositoryConfig.class})
public class SystemTestConfig {
@Bean
public DataSource dataSource() {
// return new DataSource
}
}
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(SystemTestConfig.class);
// everything wires up across configuration classes...
TransferService transferService = ctx.getBean(TransferService.class);
transferService.transfer(100.00, "A123", "C456");
}
Environment 인터페이스를 통해 애플리케이션 환경을 추상화하고 제어하는 기능이다.Environment Abstraction은 환경에 대한 추상화로 Spring에서 제공하는 Environment 인터페이스를 이용한다.
Environment 인터페이스는 애플리케이션 환경의 두 가지 주요 측면을 모델링하는 컨테이너에 통합된 추상화로, profiles 나 properties처럼 프로그램의 환경 변수나 Application의 프로필을 관리할 때 사용하게 된다.
Profile은 지정된 프로파일이 활성화된 경우에만 컨테이너에 등록되는 명명된 빈 정의의 논리적 그룹이다.
Bean은 XML 또는 주석으로 정의된 프로필에 할당될 수 있다. 프로필과 관련된 환경 개체의 역할은 현재 활성화된 Profile(있는 경우)과 기본적으로 활성화되어야 하는 Profile(있는 경우)을 결정하는 것이다.
Properties는 거의 모든 애플리케이션에서 중요한 역할을 하며 특성 파일, JVM 시스템 특성, 시스템 환경 변수, JNDI, 서블릿 컨텍스트 매개변수, 임시 특성 오브젝트, 맵 오브젝트 등 다양한 소스에서 생성될 수 있다.
속성과 관련된 환경 개체의 역할은 사용자에게 속성 소스를 구성하고 속성을 해결하기 위한 편리한 서비스 인터페이스를 제공하는 것이다.
Bean 정의 프로파일은 아래와 같은 상황에서 코어 컨테이너에서 서로 다른 환경에서 서로 다른 Bean을 등록할 수 있는 메커니즘을 제공한다.
@Profile 어노테이션을 사용하면 하나 이상의 지정된 프로필이 활성 상태일 때 구성 요소가 등록에 적합함을 나타낼 수 있다. @Profile를 사용하여 아래와 같이 데이터소스를 구성할 수 있다.
// standaloneDataSource는 development profile 에서만 사용
@Configuration
@Profile("development")
public class StandaloneDataConfig {
@Bean
public DataSource dataSource() {
return new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.HSQL)
.addScript("classpath:com/bank/config/sql/schema.sql")
.addScript("classpath:com/bank/config/sql/test-data.sql")
.build();
}
}
// jndiDataSource는 production profile 에서만 사용
@Configuration
@Profile("production")
public class JndiDataConfig {
@Bean(destroyMethod="")
public DataSource dataSource() throws Exception {
Context ctx = new InitialContext();
return (DataSource) ctx.lookup("java:comp/env/jdbc/datasource");
}
}
XML <bean/> 에서 제공하는 profile 요소를 이용하여 Profile을 구성할 수 있다. 위의 데이터소스 예제를 아래와 같이 변경할 수 있다.
<beans profile="development"
xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:jdbc="http://www.springframework.org/schema/jdbc"
xsi:schemaLocation="...">
<jdbc:embedded-database id="dataSource">
<jdbc:script location="classpath:com/bank/config/sql/schema.sql"/>
<jdbc:script location="classpath:com/bank/config/sql/test-data.sql"/>
</jdbc:embedded-database>
</beans>
<beans profile="production"
xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:jee="http://www.springframework.org/schema/jee"
xsi:schemaLocation="...">
<jee:jndi-lookup id="dataSource" jndi-name="java:comp/env/jdbc/datasource"/>
</beans>
그리고, 아래와 같이 어떤 Profile을 활성화할지 설정할 수 있다.
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.getEnvironment().setActiveProfiles("development");
ctx.register(SomeConfig.class, StandaloneDataConfig.class, JndiDataConfig.class);
ctx.refresh();
@PropertySource 어노테이션은 PropertySource를 Spring의 환경에 추가하기 위한 편리하고 선언적인 메커니즘을 제공한다. testbean.name=myTestBean(키-값)을 포함하는 app.properties라는 파일이 주어지면 @Configuration 클래스는 testBean.getName()에 대한 호출이 myTestBean을 반환하는 방식으로 @PropertySource를 사용한다.
@Configuration
@PropertySource("classpath:/com/myco/app.properties")
public class AppConfig {
@Autowired
Environment env;
@Bean
public TestBean testBean() {
TestBean testBean = new TestBean();
testBean.setName(env.getProperty("testbean.name"));
return testBean;
}
}
이미 등록된 속성(예: 시스템 속성 또는 환경 변수)이 있는 경우 기본값과 함께 ${…} 자리 표시자에 기술할 수 있다. 기본값이 지정되지 않고 속성을 확인할 수 없는 경우는 IllegalArgumentException이 발생한다. 아래 예제는 my.placeholder 속성이 있는 경우 my.placeholder 속성값을 사용하고 없을 경우 기본값인 default/path를 사용하겠다고 기술한 것이다.
@Configuration
@PropertySource("classpath:/com/${my.placeholder:default/path}/app.properties")
public class AppConfig {
@Autowired
Environment env;
@Bean
public TestBean testBean() {
TestBean testBean = new TestBean();
testBean.setName(env.getProperty("testbean.name"));
return testBean;
}
}
본 문서는 Martin Fowler가 저술한 Inversion of Control 문서를 번역 및 일부 의역한 것이다.
Inversion of Control(IoC)는 당신이 프레임워크를 확장할 때 마주치게 되는 일반적인 사상이다. 또한, 프레임워크를 정의하는 특징이기도 하다.
간단한 예제를 생각해보자. 명령줄의 질문을 통해 사용자로부터 어떠한 정보를 입력받는 프로그램을 작성한다고 생각해보자. 나는 아마 다음과 같은 것을 작성할 것이다.
#ruby
puts 'What is your name?'
name = gets
process_name(name)
puts 'Waht is your quest?'
quest = gets
process_quest(quest)
위 예제에서, 내가 작성한 코드는 제어권을 가지고 있다 : 질문을 언제 할 것인지, 대답은 언제 읽을 것인지, 그리고 그런 결과들을 언제 처리할 것인지 등을 결정하고 있다.
만약 같은 일을 하기 위해서 윈도우 시스템을 사용한다면, 나는 다음과 같이 윈도우를 설정할 것이다.
require 'tk'
root = TkRoot.new()
name_label = TkLabel.new() {text "What is Your Name?"}
name_label.pack
name = TkEntry.new(root).pack
name.bind("FocusOut") {process_name(name)}
quest_label = TkLabel.new() {text "What is Your Quest?"}
quest_label.pack
quest = TkEntry.new(root).pack
quest.bind("FocusOut") {process_quest(quest)}
Tk.mainloop()
위 두 프로그램은 제어의 흐름에 있어서 큰 차이점을 가지고 있다. 특히 process_name과 process_quest 메소드가 호출되는 시점에 대한 제어가 다르다. 명령줄 방식을 사용한 프로그램의 경우, 나는 메소드들이 호출되는 시점을 직접 제어했다. 하지만, 윈도우를 사용한 프로그램은 그러지 않았다. 대신 나는 윈도우 시스템에게 제어권을 넘겨주었다(Tk.mainloop 명령어를 사용하여). 윈도우 시스템은 내가 폼을 생성할 때 만든 결합 정보를 이용하여 내 메소드들을 호출할 시점을 결정한다. 즉, 제어가 역전된 것이다 - 내가 프레임워크를 호출하는 것이 아니라 프레임워크가 나를 호출하는 것이다. 이 사상이 Inversion of Control이다(또한 Hollywood Principle - “Dont’ call us, we’ss call you”라고도 한다).
프레임워크의 가장 중요한 특징은, 사용자가 프레임워크를 사용하기 위해 만든 메소드들이 사용자 어플리케이션 코드에서 호출되는 것보다 프레임워크에 의해 호출되는 것이 더 종종 일어난다는 것이다. 프레임워크는 어플리케이션의 활동을 조합하고 순차적으로 수행하는 메인 프로그램의 역할을 수행한다. 이러한 제어의 역전이 프레임워크가 확장가능한 뼈대로서의 기능을 수행할 수 있도록 해준다. 사용자는 프레임워크가 정의한 일반적인 알고리즘을 확장하여 특정 어플리케이션을 위한 메소드를 생성한다.
Inversion of Control은 프레임워크를 라이브러리와 구별짓게 만드는 핵심이다. 라이브러리는 본질적으로 당신이 호출할 수 있는 기능들의 집합이다(요즘은 이러한 기능들이 클래스를 구성하고 있다). 한번 호출되면 작업을 수행하고 클라이언트에게 다시 제어권을 넘긴다.
프레임워크는 일부 추상적인 설계를 가지고 있으며, 미리 정의된 행동 방식을 가지고 있다. 프레임워크를 사용하기 위해서 당신은 프레임워크가 제공하는 클래스를 상속하거나 또는 작성한 클래스를 프레임워크에 삽입함으로써 프레임워크에 존재하는 확장 지점에 당신이 원하는 행동 방식을 삽입해야 한다. 그러면 프레임워크는 각각의 확장 지점에서 당신의 코드를 호출할 것이다.
당신이 만든 코드를 삽입하는 방식에는 여러가지가 있다. 위 ruby 예제의 경우, 우리는 이벤트 이름과 Closure를 변수로 갖는 text entry field의 bind 메소드를 호출했다. Text entry box는 이벤트를 감지할 때 마다 closure의 코드를 호출한다. 이처럼 closure를 이용하는 것은 매우 간편하지만, 이를 지원하는 언어는 많지 않다.
또다른 방법으로는 프레임워크가 이벤트를 정의하고, 클라이언트가 이들 이벤트를 받는 방법이 있다 .NET 플랫폼이 이벤트를 선언할 수 있는 언어적 특징을 가진 좋은 예이다. 당신은 delegate를 사용하여 메소드와 이벤트를 연결할 수 있다.
위 방식(실제로는 둘은 같다)은 단순한 경우에는 잘 동작한다. 하지만 때때로 당신은 하나의 확장 지점에서 여러개의 메소드를 접합하기를 원할 수도 있다. 이런 경우 프레임워크는 인터페이스를 정의하고 클라이언트가 이를 구현할 수 있다.
EJB가 이런 inversion of control 형식의 좋은 예이다. 당신이 session bean을 개발할 때, 당신은 여러 생명주기 지점에서 EJB 컨테이너에 의해 호출되는 다양한 메소드를 구현할 수 있다. 예를 들어, Session Bean 인터페이스는 ejbRemote, ejbPassivate(2차 저장소에 저장됨), 그리고 ejbActivate(비활성 상태에서 복원됨)를 정의한다. 당신은 이 메소드들이 호출되는 시점에 대한 제어권을 가지지 않고, 다만 무엇을 할 것인지만 결정한다. 컨테이너가 우리를 호출하고, 우리는 그러지 않는다.
Inversion of Control의 복잡한 경우가 존재하지만, 당신은 좀더 단순한 상황에서 효과를 볼 수 있다. Template method가 좋은 예이다: 부모클래스는 제어의 흐름을 정의하고, 자식클래스는 메소드를 재정의하거나 추상메소드를 구현함으로써 확장할 수 있다. JUnit에서, 프레임워크는 당신이 테스트 기반을 생성하고 삭제하기 위해 작성한 setUp과 tearDown 메소드를 호출한다. 프레임워크가 호출하면, 당신의 코드는 반응한다. 즉, 제어가 역전된 것이다.
요즘 IoC 컨테이너의 등장에 따라 inversion of control의 의미에 대한 일부 혼동이 발생하고 있다. 몇몇 사람들은 이 문서에서 설명한 일반적인 원리와 이들 컨테이너에서 사용하고 있는 inversion of control의 특수한 형식인 dependency injection을 혼동하고 있다. 이름에서 약간 혼란이 야기된다고 할 수 있는데,(또한 모순적이기도 하다) IoC 컨테이너는 일반적으로 EJB의 경쟁상대로 간주되지만 EJB가 inversion of control을 더 많이 사용하기 때문이다.
어원: 내가 알기론, Inversion of Control이란 단어는 1988년 Object-Oriented Programming 저널에 발표된 Johnson and Foote의 논문 Designing Reusable Classes에서 처음 사용되었다. 이 논문은 잘 작성된 눈문 중의 하나로, 발표 이후 15년에 이른 현재까지도 읽을만한 가치가 있다. 그들은 어딘가 다른 곳에서 단어를 가져왔다고 하지만, 어디였는지는 기억하지 못한다. 이 단어는 object-oriented 커뮤니티 속으로 점점 더 녹아들었고 결국 책 Gang of Four에도 나타나게 되었다. 좀 더 화려한 별칭인 ‘Hollywood Principle’은 1983년 Mesa에 실린 Richard Sweet의 논문에서 고안된 걸로 보인다. 설계 목표에서 그는 다음과 같이 기술하고 있다. “Don’t call us, we’ll call you (Hollywood’s Law): A tool should arrange for Tajo to notify it when the user wishes to communicate some event to the tool, rather than adopt an’ ask the user for a command and execute it’ model.” John Vlissides는 column for C++ report에서 ‘Hollywood Principle’ 의 개념을 잘 설명하고 있다. (어원에 대해서 도움을 준 Brian Foote과 Ralph Johnson에게 감사드린다.)
Spring4 Generic은 Autowired 및 Qualifired를 보완하여 Generic을 지원합니다.
기존 Autowire 및 Qualifier의 기능에 대하여 확장하여 Spring4에서 추가로 지원하는 Generic 타입의 Autowire기능에 대하여 알아본다.
다음은 Customer 클래스에 Person property로 Autowire하는 예제이다.
package com.egovframe.common;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
public class Customer {
@Autowired
private Person person;
//...
}
다음과 같은경우 Person에 Autowire로 주입될수 없다.
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.5.xsd">
<bean class ="org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor"/>
<bean id="customer" class="com.egovframe.common.Customer" />
<bean id="personA" class="com.egovframe.common.Person" >
<property name="name" value="GentlemanA" />
</bean>
<bean id="personB" class="com.egovframe.common.Person" >
<property name="name" value="GentlemanB" />
</bean>
</beans>
같은 타입이 2개이므로 다음과 같은 오류를 발생시킨다.
Caused by: org.springframework.beans.factory.NoSuchBeanDefinitionException:
No unique bean of type [com.egovframe.common.Person] is defined:
expected single matching bean but found 2: [personA, personB]
Qualifier 어노테이션에 personA를 특정하여 처리 할수 있으나 개별적으로 하나하나 지정해야 하는 문제가 있다.
package com.egovframe.common;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
public class Customer {
@Autowired
@Qualifier("personA")
private Person person;
//...
}
추상화 클래스 Employee를 구현한 Manager,Admin을 Generic으로 설정한다. 이경우 Spring 4의 Generic에 대하여 Autowire 주입 대상이 된다.
package generic;
import org.springframework.beans.factory.annotation.Autowired;
public class InjectBeans {
@Autowired
Employee<Manager> emp1;
@Autowired
Employee<Admin> emp2;
public void invokeManager(){
emp1.empSection();
}
public void invokeAdmin(){
emp2.empSection();
}
}
Employee는 추상클래스이며 상속을 통하여 구현된다. Admin과 Manager는 구현클래스이며 Autowire 어노테이션에의해 자동 주입된다.
package generic;
public abstract class Employee<T> {
public void printCompanyName(){
System.out.println("Display : Company Name");
}
public abstract void empSection();
}
package generic;
public class Admin extends Employee<Admin>{
public void empSection(){
System.out.println("Display : Admin Section");
}
}
package generic;
public class Manager extends Employee<Manager>{
public void empSection(){
System.out.println("Display : Manager Section");
}
}
구현 클래스 각각의 empSection()이 수행되어 다음과 같은 결과가 출력된다.
6월 23, 2015 11:23:54 오전 org.springframework.context.annotation.AnnotationConfigApplicationContext prepareRefresh
INFO: Refreshing org.springframework.context.annotation.AnnotationConfigApplicationContext@1262a85: startup date [Tue Jun 23 11:23:54 KST 2015]; root of context hierarchy
Display : Admin Section
Display : Manager Section
Spring 3에서는 complie은 문제가 없으나 runtime시에 Generic 타입에 대한 주입기능이 없으므로 다음과 같은 오류가 발생한다.
6월 23, 2015 10:38:25 오전 org.springframework.context.annotation.AnnotationConfigApplicationContext prepareRefresh
INFO: Refreshing org.springframework.context.annotation.AnnotationConfigApplicationContext@1110f31: startup date [Tue Jun 23 10:38:25 KST 2015]; root of context hierarchy
6월 23, 2015 10:38:26 오전 org.springframework.beans.factory.support.DefaultListableBeanFactory preInstantiateSingletons
INFO: Pre-instantiating singletons in org.springframework.beans.factory.support.DefaultListableBeanFactory@17f70bf: defining beans [org.springframework.context.annotation.internalConfigurationAnnotationProcessor,org.springframework.context.annotation.internalAutowiredAnnotationProcessor,org.springframework.context.annotation.internalRequiredAnnotationProcessor,org.springframework.context.annotation.internalCommonAnnotationProcessor,springMVCConfiguration,org.springframework.context.annotation.ConfigurationClassPostProcessor.importAwareProcessor,getManager,InjectBeans,getAdmin]; root of factory hierarchy
6월 23, 2015 10:38:26 오전 org.springframework.beans.factory.support.DefaultListableBeanFactory destroySingletons
INFO: Destroying singletons in org.springframework.beans.factory.support.DefaultListableBeanFactory@17f70bf: defining beans [org.springframework.context.annotation.internalConfigurationAnnotationProcessor,org.springframework.context.annotation.internalAutowiredAnnotationProcessor,org.springframework.context.annotation.internalRequiredAnnotationProcessor,org.springframework.context.annotation.internalCommonAnnotationProcessor,springMVCConfiguration,org.springframework.context.annotation.ConfigurationClassPostProcessor.importAwareProcessor,getManager,InjectBeans,getAdmin]; root of factory hierarchy
Exception in thread "main" org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'InjectBeans': Injection of autowired dependencies failed; nested exception is org.springframework.beans.factory.BeanCreationException: Could not autowire field: generic.Employee generic.InjectBeans.emp1; nested exception is org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type [generic.Employee] is defined: expected single matching bean but found 2: getManager,getAdmin
at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor.postProcessPropertyValues(AutowiredAnnotationBeanPostProcessor.java:289)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.populateBean(AbstractAutowireCapableBeanFactory.java:1146)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:519)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:458)
at org.springframework.beans.factory.support.AbstractBeanFactory$1.getObject(AbstractBeanFactory.java:296)
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:223)
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:293)
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:194)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:633)
at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:932)
at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:479)
at org.springframework.context.annotation.AnnotationConfigApplicationContext.<init>(AnnotationConfigApplicationContext.java:73)
at generic.SpringMVCApplicationDemo.main(SpringMVCApplicationDemo.java:15)
Caused by: org.springframework.beans.factory.BeanCreationException: Could not autowire field: generic.Employee generic.InjectBeans.emp1; nested exception is org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type [generic.Employee] is defined: expected single matching bean but found 2: getManager,getAdmin
at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.inject(AutowiredAnnotationBeanPostProcessor.java:517)
at org.springframework.beans.factory.annotation.InjectionMetadata.inject(InjectionMetadata.java:87)
at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor.postProcessPropertyValues(AutowiredAnnotationBeanPostProcessor.java:286)
... 12 more
Caused by: org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type [generic.Employee] is defined: expected single matching bean but found 2: getManager,getAdmin
at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:870)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:775)
at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.inject(AutowiredAnnotationBeanPostProcessor.java:489)
... 14 more
AOP 서비스는 관점지향 프로그래밍(Aspect Oriented Programming: AOP) 사상을 구현하고 지원한다. 실행환경 AOP 서비스는 Spring AOP를 사용한다. 본 장에서는 AOP의 개요 및 Spring의 AOP 지원을 중심으로 살펴본다.
개별 프로그래밍 언어는 프로그램 개발을 위해 고유한 관심사 분리(Separation of Concerns) 패러다임을 갖는다. 예를 들면 절차적 프로그래밍은 상태값을 갖지 않는 연속된 함수들의 실행을 프로그램으로 이해하고 모듈을 주요 분리 단위로 정의한다. 객체지향 프로그래밍은 일련의 함수 실행이 아닌 상호작용하는 객체들의 집합으로 보며 클래스를 주요 단위로 한다.
객체지향 프로그래밍은 많은 장점에도 불구하고, 다수의 객체들에 분산되어 중복적으로 존재하는 공통 관심사가 존재한다. 이들은 프로그램을 복잡하게 만들고, 코드의 변경을 어렵게 한다.
관점 지향 프로그래밍(AOP, Aspect-Oriented Programming)은 이러한 객체지향 프로그래밍의 문제점을 보완하는 방법으로 핵심 관심사를 분리하여 프로그램 모듈화를 향상시키는 프로그래밍 스타일이다. AOP는 객체를 핵심 관심사와 횡단 관심사로 분리하고, 횡단 관심사를 관점(Aspect)이라는 모듈로 정의하고 핵심 관심사와 엮어서 처리할 수 있는 방법을 제공한다.
다음 그림은 객체지향 프로그래밍 개발에서 핵심 관심사와 횡단 관심사가 하나의 코드로 통합되어 개발된 사례를 보여준다.

객체지향 프로그래밍 코드에 AOP를 적용하면 다음 그림처럼 각 코드에 분산되어 있던 횡단 관심사는 관점으로 분리되어 정의된다. AOP는 엮기(Weaving)라는 방식을 이용하여 분리된 관점을 핵심 관심사와 엮는다.

관점 지향 프로그래밍은 횡단 관심사를 분리하고 핵심 관심사와 엮어 사용할 수 있는 방법을 제공하며 다음의 몇 가지 새로운 개념을 포함한다.
관점은 구현하고자 하는 횡단 관심사의 기능이다.
결합점은 관점(Aspect)를 삽입하여 실행 가능한 어플리케이션의 특정 지점을 말한다.
포인트컷은 결합점 집합을 의미한다. 포인트컷은 어떤 결합점을 사용할 것이지를 결정하기 위해 패턴 매칭을 이용하여 룰을 정의한다. 다음 그림은 Spring 2.5에 포함된 bean() 포인트컷을 이용하여 종적 및 횡적으로 빈을 선택하는 예제를 보여준다.

충고(Advice)는 관점(Aspect)의 실제 구현체로 결합점에 삽입되어 동작할 수 있는 코드이다. 충고는 결합점과 결합하여 동작하는 시점에 따라 before advice, after advice, around advice 타입으로 구분된다.
엮기는 관점(Aspect)을 대상 객체에 적용하여 새로운 프록시 객체를 생성하는 과정이다. 엮기 방식은 다음과 같이 구분된다.
도입(Introduction)은 새로운 메소드나 속성을 추가한다. Spring AOP는 충고(Advice)를 받는 대상 객체에 새로운 인터페이스를 추가할 수 있다.
AOP 프록시(Proxy)는 대상 객체(Target Object)에 Advice가 적용된 후 생성되는 객체이다.
대상 객체는 충고(Advice)를 받는 객체이다. Spring AOP는 런타임 프록시를 사용하므로 대상 객체는 항상 프록시 객체가 된다.
스프링은 프록시 기반의 런타임 Weaving 방식을 지원한다. 스프링은 AOP 구현을 위해 다음 세가지 방식을 제공한다. 이 중 @AspectJ 어노테이션과 XML 스키마를 이용한 AOP 방식을 상세히 살펴본다.
@AspectJ는 Java 5 어노테이션을 사용한 일반 Java 클래스로 관점(Aspect)를 정의하는 방식이다. @AspectJ 방식은 AspectJ 5 버전에서 소개되었으며, Spring은 2.0 버전부터 AspectJ 5 어노테이션을 지원한다. Spring AOP 실행환경은 AspectJ 컴파일러나 직조기(Weaver)에 대한 의존성이 없이 @AspectJ 어노테이션을 지원한다.
@AspectJ를 사용하기 위해서 다음 코드를 Spring 설정에 추가한다.
<aop:aspectj-autoproxy/>
클래스에 @Aspect 어노테이션을 추가하여 Aspect를 생성한다. @Aspect 설정이 되어 있는 경우 Spring은 자동적으로 @Aspect 어노테이션을 포함한 클래스를 검색하여 Spring AOP 설정에 반영한다.
import org.aspectj.lang.annotation.Aspect;
@Aspect
public class AspectUsingAnnotation {
..
}
포인트컷은 결합점(Join points)을 지정하여 충고(Advice)가 언제 실행될지를 지정하는데 사용된다. Spring AOP는 Spring 빈에 대한 메소드 실행 결합점만을 지원하므로, Spring에서 포인트컷은 빈의 메소드 실행점을 지정하는 것으로 생각할 수 있다. 다음 예제는 egovframework.rte.fdl.aop.sample 패키지 하위의 Sample 명으로 끝나는 클래스의 모든 메소드 수행과 일치할 ’targetMethod’ 라는 이름의 pointcut을 정의한다.
@Aspect
public class AspectUsingAnnotation {
...
@Pointcut("execution(public * org.egovframe.rte.fdl.aop.sample.*Sample.*(..))")
public void targetMethod() {
// pointcut annotation 값을 참조하기 위한 dummy method
}
...
}
Spring에서 포인트컷 표현식에 사용될 수 있는 지정자는 다음과 같다. 포인트컷은 모두 public 메소드를 대상으로 한다.
포인트컷 표현식은 ‘&&’, ‘||’ 그리고 ‘!’ 를 사용하여 조합할 수 있다.
@Pointcut("execution(public * *(..))")
private void anyPublicOperation() {}
@Pointcut("within(com.xyz.someapp.trading..*)")
private void inTrading() {}
@Pointcut("anyPublicOperation() && inTrading()")
private void tradingOperation() {}
Spring AOP에서 자주 사용되는 포인트컷 표현식의 예를 살펴본다.
| Pointcut | 선택된 Joinpoints |
|---|---|
| execution(public * *(..)) | public 메소드 실행 |
| execution(* set*(..)) | 이름이 set으로 시작하는 모든 메소드명 실행 |
| execution(* set*(..)) | 이름이 set으로 시작하는 모든 메소드명 실행 |
| execution(* com.xyz.service.AccountService.*(..)) | AccountService 인터페이스의 모든 메소드 실행 |
| execution(* com.xyz.service.*.*(..)) | service 패키지의 모든 메소드 실행 |
| execution(* com.xyz.service..*.*(..)) | service 패키지와 하위 패키지의 모든 메소드 실행 |
| within(com.xyz.service.*) | service 패키지 내의 모든 결합점 |
| within(com.xyz.service..*) | service 패키지 및 하위 패키지의 모든 결합점 |
| this(com.xyz.service.AccountService) | AccountService 인터페이스를 구현하는 프록시 개체의 모든 결합점 |
| target(com.xyz.service.AccountService) | AccountService 인터페이스를 구현하는 대상 객체의 모든 결합점 |
| args(java.io.Serializable) | 하나의 파라미터를 갖고 전달된 인자가 Serializable인 모든 결합점 |
| @target(org.springframework.transaction.annotation.Transactional) | 대상 객체가 @Transactional 어노테이션을 갖는 모든 결합점 |
| @within(org.springframework.transaction.annotation.Transactional) | 대상 객체의 선언 타입이 @Transactional 어노테이션을 갖는 모든 결합점 |
| @annotation(org.springframework.transaction.annotation.Transactional) | 실행 메소드가 @Transactional 어노테이션을 갖는 모든 결합점 |
| @args(com.xyz.security.Classified) | 단일 파라미터를 받고, 전달된 인자 타입이 @Classified 어노테이션을 갖는 모든 결합점 |
| bean(accountRepository) | “accountRepository” 빈 |
| !bean(accountRepository) | “accountRepository” 빈을 제외한 모든 빈 |
| bean(*) | 모든 빈 |
| bean(account*) | 이름이 ‘account’로 시작되는 모든 빈 |
| bean(*Repository) | 이름이 “Repository”로 끝나는 모든 빈 |
| bean(accounting/*) | 이름이 “accounting/“로 시작하는 모든 빈 |
| bean(*dataSource) |
충고(Advice)는 관점(Aspect)의 실제 구현체로 포인트컷 표현식과 일치하는 결합점에 삽입되어 동작할 수 있는 코드이다. 충고는 결합점과 결합하여 동작하는 시점에 따라 before advice, after advice, around advice 타입으로 구분된다.
Before advice는 @Before 어노테이션을 사용한다. 다음은 Before 충고를 사용하는 예제이다. Before 충고인 beforeTargetMethod() 메소드는 targetMethod()로 정의된 포인트컷 전에 수행된다.
@Aspect
public class AspectUsingAnnotation {
..
@Before("targetMethod()")
public void beforeTargetMethod(JoinPoint thisJoinPoint) {
Class clazz = thisJoinPoint.getTarget().getClass();
String className = thisJoinPoint.getTarget().getClass().getSimpleName();
String methodName = thisJoinPoint.getSignature().getName();
System.out.println("AspectUsingAnnotation.beforeTargetMethod executed.");
System.out.println(className + "." + methodName + " executed.");
}
}
After returing 충고는 정상적으로 메소드가 실행될 때 수행된다. After returning 충고는 @AfterReturing 어노테이션을 사용한다. 다음은 After returning 충고를 사용하는 예제이다. afterReturningTargetMethod() 충고는 targetMethod()로 정의된 포인트컷 후에 수행된다. targetMethod() 포인트컷의 실행 결과는 retVal 변수에 저장되어 전달된다.
@Aspect
public class AspectUsingAnnotation {
..
@AfterReturning(pointcut = "targetMethod()", returning = "retVal")
public void afterReturningTargetMethod(JoinPoint thisJoinPoint, Object retVal) {
System.out.println("AspectUsingAnnotation.afterReturningTargetMethod executed." + " return value is [" + retVal + "]");
}
}
After throwing 충고는 메소드가 수행 중 예외사항을 반환하고 종료하는 경우 수행된다. After throwing 충고는 @AfterThrowing 어노테이션을 사용한다. 다음은 After throwing 충고를 사용하는 예제이다. afterThrowingTargetMethod() 충고는 targetMethod()로 정의된 포인트컷에서 예외가 발생한 후에 수행된다. targetMethod() 포인트컷에서 발생된 예외는 exception 변수에 저장되어 전달된다. 예제에서는 전달 받은 예외를 한번 더 감싸서 사용자가 쉽게 알아 볼 수 있도록 메시지를 설정하여 반환한다.
@Aspect
public class AspectUsingAnnotation {
..
@AfterThrowing(pointcut = "targetMethod()", throwing = "exception")
public void afterThrowingTargetMethod(JoinPoint thisJoinPoint,
Exception exception) throws Exception {
System.out.println("AspectUsingAnnotation.afterThrowingTargetMethod executed.");
System.out.println("에러가 발생했습니다.", exception);
throw new BizException("에러가 발생했습니다.", exception);
}
}
After (finally) 충고는 메소드 수행 후 무조건 수행된다. After (finally) 충고는 @After 어노테이션을 사용한다. After 충고는 정상 종료와 예외 발생 경우를 모두 처리해야 하는 경우에 사용된다. 리소스 해제와 같은 작업이 해당된다. 다음은 After (finally) 충고를 사용하는 예제이다. afterTargetMethod() 충고는 targetMethod()로 정의된 포인트컷 이후에 수행된다.
@Aspect
public class AspectUsingAnnotation {
..
@After("targetMethod()")
public void afterTargetMethod(JoinPoint thisJoinPoint) {
System.out.println("AspectUsingAnnotation.afterTargetMethod executed.");
}
}
Around 충고는 메소드 수행 전후에 수행된다. Around 충고는 @Around 어노테이션을 사용한다. 다음은 Around 충고를 사용하는 예제이다. aroundTargetMethod() 충고는 파라미터로 ProceedingJoinPoint을 전달하며 proceed() 메소드 호출을 통해 대상 포인트컷을 실행한다. 포인트컷 수행 결과값인 retVal을 Around 충고 내에서 변환하여 반환할 수 있음을 보여준다.
@Aspect
public class AspectUsingAnnotation {
..
@Around("targetMethod()")
public Object aroundTargetMethod(ProceedingJoinPoint thisJoinPoint) throws Throwable {
System.out.println("AspectUsingAnnotation.aroundTargetMethod start.");
long time1 = System.currentTimeMillis();
Object retVal = thisJoinPoint.proceed();
System.out.println("ProceedingJoinPoint executed. return value is [" + retVal + "]");
retVal = retVal + "(modified)";
System.out.println("return value modified to [" + retVal + "]");
long time2 = System.currentTimeMillis();
System.out.println("AspectUsingAnnotation.aroundTargetMethod end. Time(" + (time2 - time1) + ")");
return retVal;
}
}
앞서 정의한 관점(Aspect)가 정상적으로 동작하는지 확인하기 위해 테스트 코드를 이용해 확인해 본다. AnnotationAspectTest 클래스는 대상 메소드 수행 시 예외 없이 정상 실행하는 경우와 예외 발생의 경우를 구분해서 테스트한다.
testAnnotationAspect() 함수는 대상 메소드가 정상 수행되는 사례를 보여준다. egovframework.rte.fdl.aop.sample 패키지에 속하는 AnnotationAdviceSample 클래스의 someMethod() 메소드는 before, after returning, after finally, around 충고(Advice)가 적용된다.
public class AnnotationAspectTest {
@Resource(name = "annotationAdviceSample")
AnnotationAdviceSample annotationAdviceSample;
@Test
public void testAnnotationAspect() throws Exception {
SampleVO vo = new SampleVO();
..
String resultStr = annotationAdviceSample.someMethod(vo);
assertEquals("someMethod executed.(modified)", resultStr);
}
}
테스트 코드를 수행한 결과 로그는 다음과 같다.
AspectUsingAnnotation.beforeTargetMethod executed.
AspectUsingAnnotation.aroundTargetMethod start.
ProceedingJoinPoint executed. return value is [someMethod executed.]
return value modified to [someMethod executed.(modified)]
AspectUsingAnnotation.aroundTargetMethod end. Time(78)
AspectUsingAnnotation.afterTargetMethod executed.
AspectUsingAnnotation.afterReturningTargetMethod executed. return value is [someMethod executed.(modified)]
콘솔 로그 출력을 보면 충고(Advice)가 적용되는 순서는 다음과 같다.
주의할 점은 @Around 충고는 대상 메소드의 반환 값(return value)를 변경 가능하지만, After returning 충고는 반환 값을 참조 가능하지만 변경할 수 없다.
testAnnotationAspectWithException() 함수는 대상 메소드에 오류가 발생한 사례를 보여준다. egovframework.rte.fdl.aop.sample 패키지에 속하는 AnnotationAdviceSample 클래스의 someMethod() 메소드는 before, after throwing, after finally, around 충고(Advice)가 적용된다.
public class AnnotationAspectTest {
@Resource(name = "annotationAdviceSample")
AnnotationAdviceSample annotationAdviceSample;
@Test
public void testAnnotationAspectWithException() throws Exception {
SampleVO vo = new SampleVO();
// exception 을 발생시키도록 플래그 설정
vo.setForceException(true);
..
try {
// vo 의 forceException 플래그가 true 이면 - / by zero 상황을 강제로 처리함
resultStr = annotationAdviceSample.someMethod(vo);
fail("exception 을 강제로 발생시켜 이 라인이 수행될 수 없습니다.");
} catch (Exception e) {
..
}
}
}
테스트 코드를 수행한 결과 로그는 다음과 같다.
AspectUsingAnnotation.beforeTargetMethod executed.
AspectUsingAnnotation.aroundTargetMethod start.
AspectUsingAnnotation.afterTargetMethod executed.
AspectUsingAnnotation.afterThrowingTargetMethod executed.
에러가 발생했습니다.
java.lang.ArithmeticException: / by zero
...
콘솔 로그 출력을 보면 충고(Advice)가 적용되는 순서는 다음과 같다.
예외가 발생하더라도 after 로 정의한 충고(Advice)는 수행되는 것을 확인할 수 있다. After Throwing 충고(Advice)는 에러 메시지를 재설정하고 새로운 예외를 생성하여 전달할 수 있다.
aop 네임스페이스를 제공한다. 이 방식에서도 @AspectJ AOP에서 사용된 포인트컷 표현식과 충고(Advice) 유형을 동일하게 사용할 수 있다.Java 5 버전을 사용할 수 없거나, XML 기반 설정을 선호한다면, Spring 2.0 이상에서 제공하는 XML 스키마 기반의 AOP를 사용할 수 있다. Spring은 관점(Aspect) 정의를 지원하기 위해 “aop” 네임스페이스를 제공한다. @AspectJ를 이용한 AOP 지원에서 사용된 포인트컷 표현식과 충고(Advice) 유형은 XML 스키마 기반 AOP 지원에도 동일하게 제공된다.
Spring 어플리케이션 컨텍스트에서 빈으로 정의된 일반 Java 개체는 관점(Aspect)으로 정의될 수 있다. 관점(Aspect)은 <aop:aspect> 요소를 사용하여 정의한다.
<bean id="adviceUsingXML" class="org.egovframe.rte.fdl.aop.sample.AdviceUsingXML" />
<aop:config>
<aop:aspect ref="adviceUsingXML">
...
</aop:aspect>
</aop:config>
관점(Aspect)로 정의된 aBean은 Spring 빈처럼 설정되고 의존성 주입이 될 수 있다.
포인트컷은 결합점(Join points)을 지정하여 충고(Advice)가 언제 실행될지를 지정하는데 사용된다. Spring AOP는 Spring 빈에 대한 메소드 실행 결합점만을 지원하므로, Spring에서 포인트컷은 빈의 메소드 실행점을 지정하는 것으로 생각할 수 있다. 다음 예제는 egovframework.rte.fdl.aop.sample 패키지 하위의 Sample 명으로 끝나는 클래스의 모든 메소드 수행과 일치할 ’targetMethod’ 라는 이름의 pointcut을 정의한다. 포인트컷은 <aop:config> 요소 내에 정의한다. 포인트컷 표현식은 AspectJ 포인트컷 표현 언어와 동일하게 사용할 수 있다.
<aop:config>
<aop:pointcut id="targetMethod" expression="execution(* org.egovframe.rte.fdl.aop.sample.*Sample.*(..))" />
</aop:config>
충고(Advice)는 관점(Aspect)의 실제 구현체로 포인트컷 표현식과 일치하는 결합점에 삽입되어 동작할 수 있는 코드이다. 충고는 결합점과 결합하여 동작하는 시점에 따라 before advice, after advice, around advice 타입으로 구분된다. @AspectJ를 이용한 AOP와 동일하게 5종류의 충고(Advice)를 지원한다.
Before advice는 <aop:aspect> 요소 내에서 <aop:before> 요소를 사용하여 정의한다. 다음은 before 충고를 정의하는 XML의 예제이다. before 충고인 beforeTargetMethod() 메소드는 targetMethod()로 정의된 포인트컷 전에 수행된다.
<aop:aspect ref="adviceUsingXML">
<aop:before pointcut-ref="targetMethod" method="beforeTargetMethod" />
</aop:aspect>
다음은 before 충고를 구현하고 있는 클래스이다. before 충고를 수행하는 beforeTargetXML()메소드는 해당 포인트컷을 가진 클래스명과 메소드 명을 출력한다.
public class AdviceUsingXML {
...
public void beforeTargetMethod(JoinPoint thisJoinPoint) {
System.out.println("AdviceUsingXML.beforeTargetMethod executed.");
Class clazz = thisJoinPoint.getTarget().getClass();
String className = thisJoinPoint.getTarget().getClass().getSimpleName();
String methodName = thisJoinPoint.getSignature().getName();
System.out.println(className + "." + methodName + " executed.");
}
...
}
After returning 충고는 정상적으로 메소드가 실행될 때 수행된다. After 충고는 <aop:aspect> 요소 내에서 <aop:after-returning> 요소를 사용하여 정의한다. 다음은 After returning 충고를 사용하는 예제이다. afterReturningTargetMethod() 충고는 targetMethod()로 정의된 포인트컷 후에 수행된다. targetMethod() 포인트컷의 실행 결과는 retVal 변수에 저장되어 전달된다.
<aop:aspect ref="adviceUsingXML">
<aop:after-returning pointcut-ref="targetMethod" method="afterReturningTargetMethod" returning="retVal" />
</aop:aspect>
다음은 After returning 충고를 구현하고 있는 클래스이다. After returning 충고를 수행하는 afterReturningTargetMethod()메소드는 해당 포인트컷의 반환값을 출력한다.
public class AdviceUsingXML {
...
public void afterReturningTargetMethod(JoinPoint thisJoinPoint, Object retVal) {
System.out.println("AdviceUsingXML.afterReturningTargetMethod executed." + return value is [" + retVal + "]");
}
...
}
After throwing 충고는 메소드가 수행 중 예외사항을 반환하고 종료하는 경우 수행된다. After 충고는 <aop:aspect> 요소 내에서 <aop:after-returning> 요소를 사용하여 정의한다. 다음은 After throwing 충고를 사용하는 예제이다. afterThrowingTargetMethod() 충고는 targetMethod()로 정의된 포인트컷에서 예외가 발생한 후에 수행된다. targetMethod() 포인트컷에서 발생된 예외는 exception 변수에 저장되어 전달된다.
<aop:aspect ref="adviceUsingXML">
<aop:after-throwing pointcut-ref="targetMethod" method="afterThrowingTargetMethod" throwing="exception" />
</aop:aspect>
다음은 After throwing 충고를 구현하고 있는 클래스이다. After throwing 충고를 수행하는 afterReturningTargetMethod()메소드는 전달 받은 예외를 한번 더 감싸서 사용자가 쉽게 알아 볼 수 있도록 메시지를 설정하여 반환한다.
public class AdviceUsingXML {
...
public void afterThrowingTargetMethod(JoinPoint thisJoinPoint, Exception exception) throws Exception {
System.out.println("AdviceUsingXML.afterThrowingTargetMethod executed.");
System.err.println("에러가 발생했습니다.", exception);
throw new BizException("에러가 발생했습니다.", exception);
}
...
}
After (finally) 충고는 메소드 수행 후 무조건 수행된다. After 충고는 <aop:aspect> 요소 내에서 <aop:after> 요소를 사용하여 정의한다. After 충고는 다음은 After (finally) 충고를 사용하는 예제이다. afterTargetMethod() 충고는 targetMethod()로 정의된 포인트컷의 정상 종료 및 예외 발생의 경우 모두에 대해 수행된다. 보통은 리소스 해제와 같은 작업을 수행한다.
<aop:aspect ref="adviceUsingXML">
<aop:after pointcut-ref="targetMethod" method="afterTargetMethod" />
</aop:aspect>
다음은 After (finally) 충고를 구현하고 있는 클래스이다. After (finally) 충고를 수행하는 afterTargetMethod()메소드는 after 충고가 수행됨을 표시하는 메시지를 출력한다.
public class AdviceUsingXML {
...
public void afterTargetMethod(JoinPoint thisJoinPoint) {
System.out.println("AdviceUsingXML.afterTargetMethod executed.");
}
...
}
Around 충고는 메소드 수행 전후에 수행된다. After 충고는 <aop:aspect> 요소 내에서 <aop:around> 요소를 사용하여 정의한다. Around 충고는 정상 종료와 예외 발생 경우를 모두 처리해야 하는 경우에 사용된다. 리소스 해제와 같은 작업이 해당된다.
<aop:aspect ref="adviceUsingXML">
<aop:around pointcut-ref="targetMethod" method="aroundTargetMethod" />
</aop:aspect>
다음은 Around 충고를 구현하고 있는 클래스이다. aroundTargetMethod() 충고는 파라미터로 ProceedingJoinPoint을 전달하며 proceed() 메소드 호출을 통해 대상 포인트컷을 실행한다. 포인트컷 수행 결과값인 retVal을 Around 충고 내에서 변환하여 반환할 수 있음을 보여준다.
public class AdviceUsingXML {
...
public Object aroundTargetMethod(ProceedingJoinPoint thisJoinPoint) throws Throwable {
System.out.println("AdviceUsingXML.aroundTargetMethod start.");
long time1 = System.currentTimeMillis();
Object retVal = thisJoinPoint.proceed();
System.out.println("ProceedingJoinPoint executed. return value is [" + retVal + "]");
retVal = retVal + "(modified)";
System.out.println("return value modified to [" + retVal + "]");
long time2 = System.currentTimeMillis();
System.out.println("AdviceUsingXML.aroundTargetMethod end. Time(" + (time2 - time1) + ")");
return retVal;
}
...
}
앞서 정의한 관점(Aspect)가 정상적으로 동작하는지 확인하기 위해 테스트 코드를 이용해 확인해 본다. AdviceTest 클래스는 대상 메소드 수행 시 예외 없이 정상 실행하는 경우와 예외 발생의 경우를 구분해서 테스트한다.
testAdvice() 함수는 대상 메소드가 정상 수행되는 사례를 보여준다. egovframework.rte.fdl.aop.sample 패키지에 속하는 AdviceSample 클래스의 someMethod() 메소드는 before, after returning, after finally, around 충고(Advice)가 적용된다.
public class AdviceTest{
@Resource(name = "adviceSample")
AdviceSample adviceSample;
@Test
public void testAdvice() throws Exception {
SampleVO vo = new SampleVO();
..
String resultStr = adviceSample.someMethod(vo);
assertEquals("someMethod executed.(modified)", resultStr);
}
}
테스트 코드를 수행한 결과 로그는 다음과 같다.
AdviceUsingXML.beforeTargetMethod executed.
AdviceSample.someMethod executed.
AdviceUsingXML.aroundTargetMethod start.
AdviceUsingXML.afterReturningTargetMethod executed. return value is [someMethod executed.]
AdviceUsingXML.afterTargetMethod executed.
ProceedingJoinPoint executed. return value is [someMethod executed.]
return value modified to [someMethod executed.(modified)]
AdviceUsingXML.aroundTargetMethod end. Time(12)
콘솔 로그 출력을 보면 충고(Advice)가 적용되는 순서는 다음과 같다.
주의할 점은 @Around 충고는 대상 메소드의 반환 값(return value)를 변경 가능하지만, After returning 충고는 반환 값을 참조 가능하지만 변경할 수 없다.
testAnnotationAspectWithException() 함수는 대상 메소드에 오류가 발생한 사례를 보여준다. egovframework.rte.fdl.aop.sample 패키지에 속하는 AnnotationAdviceSample 클래스의 someMethod() 메소드는 before, after throwing, after finally, around 충고(Advice)가 적용된다.
public class AdviceTest{
@Resource(name = "adviceSample")
AdviceSample adviceSample;
@Test
public void testAdviceWithException() throws Exception {
SampleVO vo = new SampleVO();
// exception 을 발생시키도록 플래그 설정
vo.setForceException(true);
...
try {
// vo 의 forceException 플래그가 true 이면 - / by zero 상황을 강제로 처리함
resultStr = adviceSample.someMethod(vo);
fail("exception 을 강제로 발생시켜 이 라인이 수행될 수 없습니다.");
} catch (Exception e) {
...
}
}
}
테스트 코드를 수행한 결과 로그는 다음과 같다.
AdviceUsingXML.beforeTargetMethod executed.
AdviceSample.someMethod executed.
AdviceUsingXML.aroundTargetMethod start.
AdviceUsingXML.afterThrowingTargetMethod executed.
에러가 발생했습니다.
java.lang.ArithmeticException: / by zero
...
콘솔 로그 출력을 보면 충고(Advice)가 적용되는 순서는 다음과 같다.
예외가 발생하더라도 after 로 정의한 충고(Advice)는 수행되는 것을 확인할 수 있다. After Throwing 충고(Advice)는 에러 메시지를 재설정하고 새로운 예외를 생성하여 전달할 수 있다.
전자정부 실행환경은 XML Schema에 기반한 AOP 방법을 사용하며, 예외처리와 트랜잭션 처리에 적용하였다. XML Schema에 기반한 AOP 방법은 @AspectJ Annotation 기반 방법에 비해 횡단 관심사에 대한 설정관계를 파악하기 유리하다.
실행환경은 DAO에서 발생한 Exception을 받아 Service단에서 처리할 수 있다. 실행환경에서 추가로 제공하는 Exception은 다음과 같다.
예외 처리를 위한 Spring 설정 파일(resources/egovframework.spring/context-aspect.xml) 내에 관점(Aspect) 클래스를 빈으로 정의한 뒤, 해당 관점(Aspect)에 대한 포인트컷과 충고(Advice)를 정의한다.
<bean id="exceptionTransfer" class="org.egovframe.rte.fdl.cmmn.aspect.ExceptionTransfer">
...
</bean>
<aop:config>
<aop:pointcut id="serviceMethod" expression="execution(* org.egovframe.rte.sample.service..*Impl.*(..))" />
<aop:aspect ref="exceptionTransfer">
<aop:after-throwing throwing="exception" pointcut-ref="serviceMethod" method="transfer" />
</aop:aspect>
</aop:config>
...
</beans>
ExceptionTransfer는 org.egovframe.rte.sample.service 패키지 내에 속한 모든 클래스 중 클래스명이 Impl로 끝나는 클래스의 메소드 실행시 발생한 예외를 처리하는 역할을 수행한다.
충고(Advice)로 정의된 ExceptionTransfer 클래스는 실행환경 소스코드에 포함되어 있다. ExceptionTransfer 클래스는 예외가 발생된 경우 내부적으로 예외처리 설정 파일에 명시된 ExceptionHandler를 호출하는 기능을 한다. ExceptionTransfer 클래스의 코드 일부는 다음과 같다.
public class ExceptionTransfer {
...
public void transfer(JoinPoint thisJoinPoint, Exception exception) throws Exception {
log.debug("execute ExceptionTransfer.transfer ");
Class clazz = thisJoinPoint.getTarget().getClass();
Locale locale = LocaleContextHolder.getLocale();
// BizException 인 경우는 이미 메시지 처리 되었음. 로그만 기록
if (exception instanceof EgovBizException) {
log.debug("Exception case :: EgovBizException ");
EgovBizException be = (EgovBizException) exception;
getLog(clazz).error(be.getMessage(), be.getCause());
// Exception Handler 에 발생된 Package 와 Exception 설정.
processHandling(clazz, exception, pm, exceptionHandlerServices, false);
throw be;
} else if (exception instanceof RuntimeException) {
log.debug("RuntimeException case :: RuntimeException ");
RuntimeException be = (RuntimeException) exception;
getLog(clazz).error(be.getMessage(), be.getCause());
// Exception Handler 에 발생된 Package 와 Exception 설정.
processHandling(clazz, exception, pm, exceptionHandlerServices, true);
if (be instanceof DataAccessException) {
log.debug("RuntimeException case :: DataAccessException ");
DataAccessException sqlEx = (DataAccessException) be;
// throw processException(clazz, "fail.data.sql",
// new String[] {
// Integer.toString(((SQLException) sqlEx
// .getCause()).getErrorCode()),
// ((SQLException) sqlEx.getCause())
// .getLocalizedMessage() }, sqlEx, locale);
throw sqlEx;
}
throw be;
} else if (exception instanceof FdlException) {
log.debug("FdlException case :: FdlException ");
FdlException fe = (FdlException) exception;
getLog(clazz).error(fe.getMessage(), fe.getCause());
throw fe;
} else {
og.debug("case :: Exception ");
getLog(clazz).error(exception.getMessage(), exception.getCause());
throw processException(clazz, "fail.common.msg", new String[] {}, exception, locale);
}
}
}
실행환경에서 트랜잭션 설정은 “resources/egovframework.spring/context-transaction.xml” 파일을 참조한다. 다음은 context-transaction.xml 설정 파일을 일부이다.
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
...
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">
<!-- 트랜잭션 관리자를 설정한다. -->
<bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<!-- 트랜잭션 Advice를 설정한다. -->
<tx:advice id="txAdvice" transaction-manager="txManager">
<tx:attributes>
<tx:method name="*" rollback-for="Exception"/>
</tx:attributes>
</tx:advice>
<!-- 트랜잭션 Pointcut를 설정한다.--->
<aop:config>
<aop:pointcut id="requiredTx" expression="execution(* org.egovframe.rte.sample..impl.*Impl.*(..))"/>
<aop:advisor advice-ref="txAdvice" pointcut-ref="requiredTx" />
</aop:config>
</beans>
리소스를 활용하여 가장 많이 사용하는 메시지 제공 서비스를 살펴본다. 메시지 제공 서비스는 미리 정의된 파일에서 메시지를 읽어 들인 후, 오류 발생시 또는 안내 메시지를 제공하기 위해 키값에 해당하는 메시지를 가져오는 기능을 제공한다.
메시지를 활용하기 위한 기본 설정 및 활용에 대해서 예제를 중심으로 설명한다.
<bean name="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource">
<property name="useCodeAsDefaultMessage">
<value>true</value>
</property>
<property name="basenames">
<list>
<value>egovframework-message</value>
</list>
</property>
</bean>
위의 설정에서”egovframework-message” 로 지정한 파일은 실제로는 egovframework-message.properties 로 정의되어 있다. 파일의 위치를 지정하는 방법이 여러가지가 가능한데 그 설정에 대한 것은 4.참고자료 참조.
//egovframework-message.properties에 정의된 메시지 내용.
resource.basic.msg1=message1
@Resource(name="messageSource")
MessageSource messageSource ;
String getMsg = messageSource.getMessage("resource.basic.msg1" , null , Locale.getDefault() );
assertEquals("Get Message Success!", getMsg , "message1");
위의 소스를 보면 messageSource.getMessage를 이용하여 Massage를 얻는 것을 확인 할 수 있다.
동일한 메시지 키를 가지고 언어별로 별도로 설정 관리하여 사용자에 따라서 사용자에 맞는 언어로 메시지를 제공할 수 있다.
<bean name="messageSource"
class="org.springframework.context.support.ResourceBundleMessageSource">
<property name="useCodeAsDefaultMessage">
<value>true</value>
</property>
<property name="basenames">
<list>
<value>egovframework-message-locale</value>
</list>
</property>
</bean>
위의 설정에서”egovframework-message-locale” 로 지정한 파일을 egovframework-message-locale_ko.properties,egovframework-message-locale_en.properties로 정의하고 동일한 메시지키에 해당하는 메시지를 달리 지정한다.
//egovframework-message-locale_ko.properties 파일 내용
resource.locale.msg1=메시지1
//egovframework-message-locale_en.properties 파일 내용
resource.locale.msg1=en_message1
위에서 resource.locale.msg1 라는 키에 다른 메시지를 설정한 것을 확인할 수 있다. 위와 같이 설정하면 locale 정보에 따라서 메시지를 제공받을 수 있다.
//egovframework-message.properties에 정의된 메시지 내용.
resource.basic.msg1=message1
String getMsg = messageSource.getMessage("resource.locale.msg1" , null , Locale.KOREAN );
assertEquals("Get Message Success!", getMsg , "메시지1");
String getMsg = messageSource.getMessage("resource.locale.msg1" , null , Locale.ENGLISH );
assertEquals("Get Message Success!", getMsg , "en_message1");
위에서 Locale정보에 따라서 추출되는 메시지의 내용이 다른 것을 확인할 수 있다.
프로그램 수행중에 발생되는 메시지를 추가하여 제공 할 수 있는데 그것에 대한 사용 방법은 설정은 위와 동일하고 Properties 파일에 아래와 같이 설정한다.
resource.basic.msg3=message {0} {1}
위에서 {0},{1}로 정의된 부분에 추가 메시지를 입력하여 제공 받을 수 있다. 위의 설정을 활용하는 샘플은 아래와 같다.
Object[] parameter = { new String("1") , new Integer(2) };
String getMsg = messageSource.getMessage("resource.basic.msg3" , parameter , Locale.getDefault() );
assertEquals("Get Message Success!", getMsg , "message 1 2");
위에서 parameter에 1과 2를 지정하여 getMessage의 두번째 인자에 넣고 호출하면 리턴 메시지로 “message 1 2”를 얻는 것을 확인 할 수 있다.
<spring:eval> 태그로 적용할 수 있다.Spring 3.0에서 처음 소개된 스프링 전용 표현식 언어로 강력하고 유연하게 사용된다.
SpEL은 빈 오브젝트에 직접 접근할 수 있는 표현식을 이용해서 프로퍼티 값을 능동적으로 가져오는 방법이며 가장 기본적이다. 또한 jsp에서 <spring:eval>태그를 사용하여 SpEL을 적용 할 수도 있다.
빈 프로퍼티에 값을 설정하면, 다른 빈이나 프로퍼티에 접근 가능하다.
<bean id="springTest" ..>
<property name="test" value="Sample" />
</bean>
<bean id="testNames">
<property name="name" value="#{springTest.test}" />
</bean>
globals.properties
driverClassName=com.mysql.jdbc.Driver
url=jdbc:mysql://localhost:1623/EASYCOMPANY
username=tex
password=texAdmin
context-datasource.xml
<util:properties id="dbprops" location="classpath:/egovframework/property/globals.properties" />
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
<property name="driverClassName" value="#{dbprops['driverClassName']}"/>
<property name="url" value="#{dbprops['url']}"/>
<property name="username" value="#{dbprops['username']}"/>
<property name="password" value="#{dbprops['password']}"/>
</bean>
@Value("#{dbprops.driverClassName}")
private String driverClassName;
또는
@Value("#{dbprops}")
private Dbproperies dbprops;
JSP의 EL대신에 Spring 3.0의 SpEL을 사용해서 값을 출력할 수 있다. JSP에서 SpEL을 사용하려면 태그 라이브러리를 추가해야 한다.
<%@ taglib prefix="spring" uri="http://www.springframework.org/tags"%>
<spring:eval> 태그를 사용하여 JSP에서 SpEL을 사용한다. 모델 오브젝트를 직접 사용할 수 있다.
<spring:eval expression="sampleVO.money"/>
메소드의 리턴값이 스트링일 경우, 메소드 자체를 호출할 수 있다.
<spring:eval expression="sampleVO.toString()"/>
또한, @NumberFormat, @DateTimeFormat과 같은 컨버전 서비스에 등록되는 포맷터를 자동으로 적용할 수 있다. 다음은 sampleVO의 일부이다.
/** 잔액 */
@NumberFormat(pattern = "###,##0")
private Integer money;
<spring:eval expression="sampleVO.money"/>
위와 같이 적용하면 입력 값에 따라 3자리마다 쉼표(,)가 출력된다.
입력값: 1234000 , 출력값: 1,234,000
모델에 직접 어노테이션으로 설정하지 않아도 new를 이용해 SpEL을 적용할 수 있다.
<spring:eval expression='new java.text.DecimalFormat("###,##0").format(price)'/>

공통기반 서비스는 실행환경 서비스 간에 공통적으로 사용되는 기능을 제공한다.
웹을 통해 데이터를 주고받는 업무를 진행할 경우, 보안상의 문제가 발생하기 쉽다.
Security Service는 웹을 통한 서비스 이용 시 발생할 수 있는 다양한 보안상의 취약점들을 사전에 인지하고 대응함으로써, 서비스의 안정성을 확보한다.
Security Service는 사용자 정보를 DB에서 관리하여 인증을 거쳐야만 접근할 수 있는 Authentication과 사용자 권한 정보를 계층화시켜서 화면 및 페이지, 또는 메소드에 접근할 수 있는 Authorization이 포함된다.
Server Security Service는 Spring Framework의 Spring Security를 확장하여 구현하였으며, 사용자 인증정보 및 권한정보를 DB에서 관리하고, Spring Security의 UserDetails 인터페이스를 확장하여 세션정보를 담을 수 있다.
Server Security의 주요기능은 다음과 같다.
전자정부 개발프레임워크의 Spring Security 기본구조와 기본 환경 설정을 설명한다.
전자정부 개발프레임워크의 Server Security는 컨테이너 기동시 적용되는 XML기반 인증이 아닌 실시간 적용되는 DB기반의 JDBC 인증을 사용한다.

리소스 요청
요청에 대해 보호되고 있는 자원인지 판단
아직 인증이 안되었으므로 HTTP 응답코드(오류) 또는 특정 페이지로 redirect
인증 메커니즘에 따라 웹 페이지 로그인 폼 또는 X509 인증서
입력 폼의 내용을 HTTP post 또는 인증 세부사항을 포함하는 HTTP 헤더를 서버로 요청
신원정보(credential)가 유효한지 판단
유효한 경우 다음단계 진행
유효하지 않을 경우 신원정보 재요청(되돌아감)
보호 자원의 접근 권한이 있을 경우 요청 성공 / 접근 권한이 없을 경우 forbidden 403 HTTP 오류



<filter>
<filter-name>springSecurityFilterChain</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
<filter-name>springSecurityFilterChain</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
사용자 인증과 관련된 테이블은 사용자테이블과 사용자권한테이블이며 사용자권한관련 테이블은 역할, 자원, 역할계층 등의 테이블이 있다.

DaoAuthenticationProvidor
CREATE TABLE USERS (
USERNAME VARCHAR(50) NOT NULL,
PASSWORD VARCHAR(50) NOT NULL,
ENABLED BIT NOT NULL,
CONSTRAINT PK_USERS PRIMARY KEY(USERNAME)
);
CREATE TABLE AUTHORITIES (
USERNAME VARCHAR(50) NOT NULL,
AUTHORITY VARCHAR(50) NOT NULL,
CONSTRAINT PK_AUTHORITIES PRIMARY KEY(USER_ID,AUTHORITY),
CONSTRAINT FK_USERS FOREIGN KEY(USER_ID) REFERENCES USERS(USER_ID),
CONSTRAINT FK_ROLES3 FOREIGN KEY(AUTHORITY) REFERENCES ROLES(AUTHORITY)
);
CREATE TABLE ROLES (
AUTHORITY VARCHAR(50) NOT NULL,
ROLE_NAME VARCHAR(50),
DESCRIPTION VARCHAR(100),
CREATE_DATE DATE,
MODIFY_DATE DATE,
CONSTRAINT PK_ROLES PRIMARY KEY(AUTHORITY)
);
| AUTHORITY | DESCRIPTION |
|---|---|
| IS_AUTHENTICATED_ANONYMOUSLY | 익명 사용자 |
| IS_AUTHENTICATED_REMEMBERED | REMEMBERED 사용자 |
| IS_AUTHENTICATED_FULLY | 인증된 사용자 |
| ROLE_RESTRICTED | 제한된 사용자 |
| ROLE_USER | 일반 사용자 |
| ROLE_ADMIN | 관리자 |
| ROLE_A | A 업무 |
| ROLE_B | B 업무 |
역할의 계층구조를 저장하는 테이블
CREATE TABLE ROLES_HIERARCHY (
PARENT_ROLE VARCHAR(50) NOT NULL,
CHILD_ROLE VARCHAR(50) NOT NULL,
CONSTRAINT PK_ROLES_HIERARCHY PRIMARY KEY(PARENT_ROLE,CHILD_ROLE),
CONSTRAINT FK_ROLES1 FOREIGN KEY(PARENT_ROLE) REFERENCES ROLES(AUTHORITY),
CONSTRAINT FK_ROLES2 FOREIGN KEY(CHILD_ROLE) REFERENCES ROLES (AUTHORITY)
);
| CHILD_ROLE | PARENT_ROLE |
|---|---|
| ROLE_ADMIN | ROLE_USER |
| ROLE_USER | ROLE_RESTRICTED |
| ROLE_RESTRICTED | IS_AUTHENTICATED_FULLY |
| IS_AUTHENTICATED_FULLY | IS_AUTHENTICATED_REMEMBERED |
| IS_AUTHENTICATED_REMEMBERED | IS_AUTHENTICATED_ANONYMOUSLY |
| ROLE_ADMIN | ROLE_A |
| ROLE_ADMIN | ROLE_B |
| ROLE_A | ROLE_RESTRICTED |
| ROLE_B | ROLE_RESTRICTED |
CREATE TABLE SECURED_RESOURCES (
RESOURCE_ID VARCHAR(10) NOT NULL,
RESOURCE_NAME VARCHAR(50),
RESOURCE_PATTERN VARCHAR(300) NOT NULL,
DESCRIPTION VARCHAR(100),
RESOURCE_TYPE VARCHAR(10),
SORT_ORDER INTEGER,
CREATE_DATE DATE,
MODIFY_DATE DATE,
CONSTRAINT PK_RECURED_RESOURCES PRIMARY KEY(RESOURCE_ID)
);
url, method, pointcut으로 자원을 보호한다.
| RESOURCE_ID | RESOURCE_PATTERN |
|---|---|
| web-000001 | \A/test\.do\Z |
| web-000002 | \A/sale/.*\.do\Z |
| web-000003 | \A/cvpl/((?!EgovCvplLogin\.do).)*\Z |
| mtd-000001 | egovframework.rte.sample.service.EgovSampleService.updateSample |
| mtd-000002 | egovframework.rte.sample.service.EgovSampleService.deleteSample |
| mtd-000003 | execution(* egovframework.rte.sample..service.*Service.insert*(..)) |
보호된 자원과 역할과의 매핑 테이블
CREATE TABLE SECURED_RESOURCES_ROLE (
RESOURCE_ID VARCHAR(10) NOT NULL,
AUTHORITY VARCHAR(50) NOT NULL,
CONSTRAINT PK_SECURED_RESOURCES_ROLE PRIMARY KEY(RESOURCE_ID,AUTHORITY),
CONSTRAINT FK_SECURED_RESOURCES FOREIGN KEY(RESOURCE_ID) REFERENCES SECURED_RESOURCES(RESOURCE_ID),
CONSTRAINT FK_ROLES4 FOREIGN KEY (AUTHORITY) REFERENCES ROLES(AUTHORITY)
);
AuthenticationManager와 AuthenticationProvider를 설정하여 사용한다. 최소한의 환경설정으로 XML 또는 DB에서 사용자 정보를 관리할 수 있으며, JDBC 인증을 통해 사용자 정보와 권한을 쿼리로 관리한다. 또한, 세션 관리를 통해 사용자 세션을 확장하고, 동시 세션 제어를 통해 동일한 ID로의 동시 접속을 제한할 수 있다.허락된 사용자에게만 공개되는 컨텐츠(정보 또는 기능)에 접근하기 위해 반드시 아이디와 암호를 입력하는 로그인 과정을 거치는데 이러한 과정이 인증(authentication)이다.
즉, 인증은 특정 사용자가 유효한 사용자인지를 판단하는 과정을 의미한다.
본 가이드에서는 인증을 위한 기본적인 환경 및 전자정부 표준프레임워크에서 사용된 인증 방법을 설명한다.
전자정부 표준프레임워크의 인증은 XML기반의 인증이 아닌 DB기반의 JDBC인증을 사용한다.
기본적인 인증 메커니즘은 인증 주체가 인증을 시도하는 초기에 오직 한 번만 인증 메커니즘이 사용되며 그 이후로는 인증 메커니즘이 정보를 필터에 유지하여 요구되는 요청을 필터 체인상의 다음 필터로 전달하기만 한다.
Spring Security에서 제공하는 인증을 사용하기 위해서는 AuthenticationManager와 실제 인증에 대한 정보를 제공하는 AuthenticationProvider의 설정이 필요하다.
AuthenticationManager는 요청을 AuthenticationProvider 체인에 전달해야 할 임무가 있다.
AuthenticationProvider는 기본적으로 UserDetails와 UserDetailsService인터페이스를 이용한다.
UserDetailsService 인터페이스 내 loadUserByUsername 메소드의 리턴된 UserDetails은 사용자명(username), 패스워드(password), 허가권한(GrantedAuthority[]) 및 사용여부(enabled)와 같은 기초적인 인증 정보들을 제공한다.
이에 전자정부 표준프레임워크에서는 UserDetails 인터페이스를 확장하였으며 JDBC 기반으로 사용자 테이블로부터 사용자 정보를 Map 또는 VO형태로 사용할 수 있도록 하였다.
최소환경설정으로 사용자 인증을 설정하는 예제이다.
<http>
<intercept-url pattern="/**" access="ROLE_USER"/>
<form-login />
<logout />
</http>
인증요청을 처리하기 위해 authentication manager가 xml(또는 DB, LDAP 등)에 정의된 사용자 정보를 사용한다.
<authentication-manager>
<authentication-provider>
<user-service>
<user name="jimi" password="jimispassword" authorities="ROLE_USER, ROLE_ADMIN" />
<user name="bob" password="bobspassword" authorities="ROLE_USER" />
</user-service>
</authentication-provider>
</authentication-manager>
표준프레임워크에서는 JdbcUserDetailsManager(org.springframework.security.provisioning)를 통해 인증 처리 부분을 JDBC 방식으로 확장하였다.
관련된 설정은 다음과 같이 지정한다.
<authentication-manager>
<authentication-provider user-service-ref="jdbcUserService">
<password-encoder hash="sha-256" base64="true"/>
</authentication-provider>
</authentication-manager>
<beans:bean id="jdbcUserService" class="egovframework.rte.fdl.security.userdetails.jdbc.EgovJdbcUserDetailsManager" >
<beans:property name="usersByUsernameQuery" value="SELECT USER_ID,PASSWORD,ENABLED,USER_NAME,BIRTH_DAY,SSN FROM USERS WHERE USER_ID = ?"/>
<beans:property name="authoritiesByUsernameQuery" value="SELECT USER_ID,AUTHORITY FROM AUTHORITIES WHERE USER_ID = ?"/>
<beans:property name="roleHierarchy" ref="roleHierarchy"/>
<beans:property name="dataSource" ref="dataSource"/>
<beans:property name="mapClass" value="egovframework.rte.fdl.security.userdetails.EgovUserDetailsMapping"/>
</beans:bean>
※ 기타 설정들은 하단 “세션관리” 참조
HTTP 폼 인증은 AuthenticationProcessingFilter를 이용하여 로그인 폼을 처리하는 것을 수반한다.
HTTP 폼 인증은 어플리케이션에서 가장 널리 사용되는 최종 사용자에 대한 인증 방법이다.
로그인 폼은 단순히 j_username과 j_password 입력 필드를 포함하며, 필터에 의해 모니터링되고 있는 URL로 게시한다.기본값은 j_spring_security_check 이다.
BASIC 인증
다이제스트 인증
익명 인증
Remember-Me 인증
X509 인증
LDAP 인증
CAS 인증
컨테이너 어댑터 인증
<http pattern="/css/**" security="none"/>
<http pattern="/images/**" security="none"/>
<http pattern="/js/**" security="none"/>
<http pattern="A/WEB-INF/jsp/.*Z" request-matcher="regex" security="none"/>
<http access-denied-page="/system/accessDenied.do" request-matcher="regex">
<form-login login-processing-url="/j_spring_security_check"
authentication-failure-url="/cvpl/EgovCvplLogin.do?login_error=1"
default-target-url="/index.jsp?flag=L"
login-page="/cvpl/EgovCvplLogin.do" />
<anonymous/>
<logout logout-success-url="/cvpl/EgovCvplLogin.do"/>
<!-- for authorization -->
<custom-filter before="FILTER_SECURITY_INTERCEPTOR" ref="filterSecurityInterceptor"/>
</http>
<authentication-manager>
<authentication-provider user-service-ref="jdbcUserService">
<password-encoder hash="sha-256" base64="true"/>
</authentication-provider>
</authentication-manager>
사용자 정보를 얻어오는 user service를 DB에 저장된 사용자 정보를 이용하여 인증할 수 있다.
<jdbc-user-service id="jdbcUserService" data-source-ref="dataSource"
users-by-username-query="SELECT USER_ID,PASSWORD,ENABLED,BIRTH_DAY FROM USERS WHERE USER_ID = ?"
authorities-by-username-query="SELECT USER_ID,AUTHORITY FROM AUTHORITIES WHERE USER_ID = ?"/>
<form action="<s:url value='/j_spring_security_check'/>" method="POST">
<table>
<tr><td>User:</td><td><input type='text' name='j_username'></td></tr>
<tr><td>Password:</td><td><input type='password' name='j_password'></td></tr>
<tr><td colspan='2' align="center"><input name="submit" type="submit" value="로그인">
<input name="reset" type="reset" value="취소"></td></tr>
</table>
</form>
Security Service의 세션 기능은 Spring Security의 JdbcUserDetailsManager 인터페이스를 확장하여 EgovJdbcUserDetailsManager 클래스를 구현하였으며
기본 테이블에 기재된 username, password, enabled 필드 외에 다른 사용자 정보를 추가하여 세션정보를 관리할 수 있다.
세션 기능을 사용하기 위해서는 JDBC 인증의 환경설정 부분을 전자정부 개발프레임워크 Server Security의 환경으로 수정해야한다.
변경전(사용안함)
<jdbc-user-service id="jdbcUserService" data-source-ref="dataSource"
users-by-username-query="SELECT USER_ID,PASSWORD,ENABLED FROM USERS WHERE USER_ID = ?"
authorities-by-username-query="SELECT USER_ID,AUTHORITY FROM AUTHORITIES WHERE USER_ID = ?"/>
변경후(사용)
<beans:bean id="jdbcUserService" class="egovframework.rte.fdl.security.userdetails.jdbc.EgovJdbcUserDetailsManager" >
<beans:property name="usersByUsernameQuery" value="SELECT USER_ID,PASSWORD,ENABLED,USER_NAME,BIRTH_DAY,SSN FROM USERS WHERE USER_ID = ?"/>
<beans:property name="authoritiesByUsernameQuery" value="SELECT USER_ID,AUTHORITY FROM AUTHORITIES WHERE USER_ID = ?"/>
<beans:property name="roleHierarchy" ref="roleHierarchy"/>
<beans:property name="dataSource" ref="dataSource"/>
<beans:property name="mapClass" value="egovframework.rte.fdl.security.userdetails.EgovUserDetailsMapping"/>
</beans:bean>
public class EgovUserDetailsVO {
private String userId;
private String passWord;
private String userName;
private String ssn;
private String lsYn;
private String birthDay;
private Integer age;
private String cellPhone;
private String addr;
private String email;
public String getUserId() {
return userId;
}
public void setUserId(String userId) {
this.userId = userId;
}
public String getPassWord() {
return passWord;
}
public void setPassWord(String passWord) {
this.passWord = passWord;
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public String getSsn() {
return ssn;
}
public void setSsn(String ssn) {
this.ssn = ssn;
}
.
.
.
public class EgovUserDetailsMapping extends EgovUsersByUsernameMapping {
public EgovUserDetailsMapping(DataSource ds, String usersByUsernameQuery) {
super(ds, usersByUsernameQuery);
}
@Override
protected Object mapRow(ResultSet rs, int rownum) throws SQLException {
String userid = rs.getString("user_id");
String password = rs.getString("password");
boolean enabled = rs.getBoolean("enabled");
String username = rs.getString("user_name");
String birthDay = rs.getString("birth_day");
String ssn = rs.getString("ssn");
EgovUserDetailsVO userVO = new EgovUserDetailsVO();
userVO.setUserId(userid);
userVO.setPassWord(password);
userVO.setUserName(username);
userVO.setBirthDay(birthDay);
userVO.setSsn(ssn);
return new EgovUserDetails(userid, password, enabled, userVO);
}
}
import egovframework.rte.fdl.security.userdetails.util.EgovUserDetailsHelper;
.
.
.
EgovUserDetailsVO user =
(EgovUserDetailsVO)EgovUserDetailsHelper.getAuthenticatedUser();
assertEquals("jimi", user.getUserId());
assertEquals("jimi test", user.getUserName());
assertEquals("19800604", user.getBirthDay());
assertEquals("1234567890123", user.getSsn());
Boolean isAuthenticated = EgovUserDetailsHelper.isAuthenticated();
assertFalse(isAuthenticated.booleanValue());
또는
assertNull(EgovUserDetailsHelper.getAuthenticatedUser());
Server security에서는 동일한 ID에 대하여 동시 접속을 제한할 수 있다.
이를 위하여 우선 web.xml에 다음과 같이 HttpSessionEventPublisher listener를 등록해 주어야 한다.
<listener>
<listener-class>
org.springframework.security.web.session.HttpSessionEventPublisher
</listener-class>
</listener>
<http access-denied-page="/system/accessDenied.do" request-matcher="regex">
<form-login login-processing-url="/j_spring_security_check"
authentication-failure-url="/cvpl/EgovCvplLogin.do?login_error=1"
default-target-url="/index.jsp?flag=L"
login-page="/cvpl/EgovCvplLogin.do" />
<anonymous/>
<logout logout-success-url="/cvpl/EgovCvplLogin.do"/>
<session-management>
<concurrency-control max-sessions="1" error-if-maximum-exceeded="true" />
</session-management>
</http>
concurrent-session-control
웹 사이트에 존재하는 모든 사용자들은 사이트 정책에 따라 그 부류 별로 컨텐츠에 대한 접근이 제한 되는데 이것을 권한 부여(authorization)라 한다. 즉, 권한은 특정 사용자가 웹 사이트에서 제공하는 컨텐츠(정보 또는 기능)에 접근 가능한지를 판단하는 과정을 의미한다.
Authorization은 XML 또는 DB에서 권한을 관리하며 계층적 권한을 지원한다.
Server Security에서는 Filter Security Interceptor에 의해 처리되며, DB로부터 권한 정보를 처리하기 위해 다음과 같이 설정된다.
<http ...>
...
<!-- for authorization -->
<custom-filter before="FILTER_SECURITY_INTERCEPTOR" ref="filterSecurityInterceptor"/>
</http>
<beans:bean id="filterSecurityInterceptor" class="org.springframework.security.web.access.intercept.FilterSecurityInterceptor">
<beans:property name="authenticationManager" ref="org.springframework.security.authenticationManager" />
<beans:property name="accessDecisionManager" ref="org.springframework.security.access.vote.AffirmativeBased#0" />
<beans:property name="securityMetadataSource" ref="databaseSecurityMetadataSource" />
</beans:bean>
...
<beans:bean id="databaseSecurityMetadataSource" class="egovframework.rte.fdl.security.intercept.EgovReloadableFilterInvocationSecurityMetadataSource">
<beans:constructor-arg ref="requestMap" />
<beans:property name="securedObjectService" ref="securedObjectService"/>
</beans:bean>
<!-- url -->
<beans:bean id="requestMap" class="egovframework.rte.fdl.security.intercept.UrlResourcesMapFactoryBean" init-method="init">
<beans:property name="securedObjectService" ref="securedObjectService"/>
</beans:bean>
<beans:bean id="securedObjectService" class="egovframework.rte.fdl.security.securedobject.impl.SecuredObjectServiceImpl">
<beans:property name="securedObjectDAO" ref="securedObjectDAO"/>
<beans:property name="requestMatcherType" value="regex"/> <!-- default : ant -->
</beans:bean>
<beans:bean id="securedObjectDAO" class="egovframework.rte.fdl.security.securedobject.impl.SecuredObjectDAO" >
<beans:property name="dataSource" ref="dataSource"/>
</beans:bean>
<beans:bean id="accessDecisionManager" class="org.springframework.security.access.vote.AffirmativeBased">
<beans:property name="allowIfAllAbstainDecisions" value="false" />
<beans:property name="decisionVoters">
<beans:list>
<beans:bean class="org.springframework.security.access.vote.RoleVoter">
<beans:property name="rolePrefix" value="ROLE_" />
</beans:bean>
<beans:bean class="org.springframework.security.access.vote.AuthenticatedVoter" />
</beans:list>
</beans:property>
</beans:bean>
요청되는 웹 url을 점검하여 DB에 저장된 url과 비교하여 접근권한을 지정할 수 있다.
\A/sale/.*\.do\Z
\A/cvpl/((?!EgovCvplLogin\.do).)*\Z
FilterSecurityInterceptor는 HTTP 자원의 보안을 처리할 책임이 있다. 다른 보안 인터셉터와 유사하게 FilterSecurityInterceptor는 AuthenticationManager와 AccessDecisionManager에 대한 참조를 필요로 한다. 또한 FilterSecurityInterceptor는 서로 다른 HTTP URL 요청에 적용되는 설정 속성도 설정한다.
<beans:bean id="filterSecurityInterceptor" class="org.springframework.security.web.access.intercept.FilterSecurityInterceptor">
<beans:property name="authenticationManager" ref="org.springframework.security.authenticationManager" />
<beans:property name="accessDecisionManager" ref="org.springframework.security.access.vote.AffirmativeBased#0" />
<beans:property name="securityMetadataSource" ref="databaseSecurityMetadataSource" />
</beans:bean>
DB 기반으로 현재 시점의 url 보호자원-권한의 맵핑 정보를 Runtime 에 동적으로 변경 반영하기 위한 Spring Security 의 FilterInvocationSecurityMetadataSource 확장 클래스이다.
<beans:bean id="databaseSecurityMetadataSource" class="egovframework.rte.fdl.security.intercept.EgovReloadableFilterInvocationSecurityMetadataSource">
<beans:constructor-arg ref="requestMap" />
<beans:property name="securedObjectService" ref="securedObjectService"/>
</beans:bean>
DB 기반의 보호자원 맵핑 정보를 얻어 이를 참조하는 빈의 초기화 데이터로 제공한다. securedObjectService의 getRolesAndUrl()를 호출하여 DB에서 역할과 url의 매핑정보를 얻어온다.
<beans:bean id="requestMap" class="egovframework.rte.fdl.security.intercept.UrlResourcesMapFactoryBean" init-method="init">
<beans:property name="securedObjectService" ref="securedObjectService"/>
</beans:bean>
Spring Security는 Spring의 DefaultAdvisorAutoProxyCreator와 함께 사용될 수 있는 MethodSecurityMetadataSourceAdvisor 처리를 제공하며, 이것을 이용하여 자동적으로 보안 인터셉터를 MethodSecurityInterceptor를 정의한 빈의 앞부분에 연결한다.
<beans:bean id="methodSecurityMetadataSourceAdvisor" class="org.springframework.security.access.intercept.aopalliance.MethodSecurityMetadataSourceAdvisor">
<beans:constructor-arg value="methodSecurityInterceptor" />
<beans:constructor-arg ref="delegatingMethodSecurityMetadataSource" />
<beans:constructor-arg value="delegatingMethodSecurityMetadataSource" />
</beans:bean>
메소드 요청에 대한 자원을 보호하기 위해 MethodSecurityInterceptor를 어플리케이션 Context에 추가해야하며 보안을 필요로 하는 빈이 인터셉터에 연결(chaining)된다. 이러한 연결은 Spring의 ProxyFactoryBean이나 BeanNameAutoProxyCreator를 이용하여 만들어지며, Spring의 여러 다른 부분들이 통상적으로 이용되는 방식과 유사하다. MethodSecurityInterceptor 는 다음과 같이 설정한다.
<beans:bean id="methodSecurityInterceptor" class="org.springframework.security.access.intercept.aopalliance.MethodSecurityInterceptor">
<beans:property name="validateConfigAttributes" value="false" />
<beans:property name="authenticationManager" ref="org.springframework.security.authenticationManager"/>
<beans:property name="accessDecisionManager" ref="org.springframework.security.access.vote.AffirmativeBased#0"/>
<beans:property name="securityMetadataSource" ref="delegatingMethodSecurityMetadataSource" />
</beans:bean>
<beans:bean id="delegatingMethodSecurityMetadataSource" class="org.springframework.security.access.method.DelegatingMethodSecurityMetadataSource">
<beans:constructor-arg>
<beans:list>
<beans:ref bean="methodSecurityMetadataSources" />
<beans:bean class="org.springframework.security.access.annotation.SecuredAnnotationSecurityMetadataSource" />
<beans:bean class="org.springframework.security.access.annotation.Jsr250MethodSecurityMetadataSource" />
</beans:list>
</beans:constructor-arg>
</beans:bean>
methodSecurityMetadataSources
<beans:bean id="methodSecurityMetadataSources" class="org.springframework.security.access.method.MapBasedMethodSecurityMetadataSource">
<beans:constructor-arg ref="methodMap" />
</beans:bean>
<beans:bean id="methodSecurityMetadataSources" class="org.springframework.security.access.method.MapBasedMethodSecurityMetadataSource">
<beans:constructor-arg ref="methodMap" />
</beans:bean>
DB 기반의 보호자원 맵핑 정보를 얻어 이를 참조하는 빈의 초기화 데이터로 제공한다. resourceType을 method로 설정하여 securedObjectService의 getRolesAndMethod()를 호출하여 DB에서 역할과 메소드의 매핑정보를 얻어온다.
<beans:bean id="methodMap" class="egovframework.rte.fdl.security.intercept.MethodResourcesMapFactoryBean" init-method="init">
<beans:property name="securedObjectService" ref="securedObjectService"/>
<beans:property name="resourceType" value="method"/>
</beans:bean>
DB 기반의 보호자원 맵핑 정보를 얻어 이를 참조하는 빈의 초기화 데이터로 제공한다. resourceType을 pointcut으로 설정하여 securedObjectService의 getRolesAndPointcut()를 호출하여 DB에서 역할과 Pointcut의 매핑정보를 얻어온다. ex: execution(* egovframework.rte.security..service.*Service.insert*(..))
<beans:bean id="protectPointcutPostProcessor" class="org.springframework.security.config.method.ProtectPointcutPostProcessor">
<beans:constructor-arg ref="methodSecurityMetadataSources" />
<beans:property name="pointcutMap" ref="pointcutMap"/>
</beans:bean>
<beans:bean id="pointcutMap" class="egovframework.rte.fdl.security.intercept.MethodResourcesMapFactoryBean" init-method="init">
<beans:property name="securedObjectService" ref="securedObjectService"/>
<beans:property name="resourceType" value="pointcut"/>
</beans:bean>
역할은 상하 계층으로 관리하며 어플리케이션 Context 또는 DB에 저장하여 관리한다.
<beans:bean id="roleHierarchy"
class="org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl" >
<beans:property name="hierarchy">
<beans:value>
ROLE_ADMIN > ROLE_USER
ROLE_USER > ROLE_RESTRICTED
ROLE_RESTRICTED > IS_AUTHENTICATED_FULLY
IS_AUTHENTICATED_REMEMBERED > IS_AUTHENTICATED_ANONYMOUSLY
</beans:value>
</beans:property>
</beans:bean>
<beans:bean id="roleHierarchy"
class="org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl" >
<beans:property name="hierarchy" ref="hierarchyStrings"/>
</beans:bean>
<beans:bean id="hierarchyStrings" class="egovframework.rte.fdl.security.userdetails.hierarchicalroles.HierarchyStringsFactoryBean" init-method="init">
<beans:property name="securedObjectService" ref="securedObjectService"/>
</beans:bean>
<beans:bean id="jdbcUserService" class="egovframework.rte.fdl.security.userdetails.jdbc.EgovJdbcUserDetailsManager" >
<beans:property name="usersByUsernameQuery" value="SELECT USER_ID,PASSWORD,ENABLED,USER_NAME,BIRTH_DAY,SSN FROM USERS WHERE USER_ID = ?"/>
<beans:property name="authoritiesByUsernameQuery" value="SELECT USER_ID,AUTHORITY FROM AUTHORITIES WHERE USER_ID = ?"/>
<beans:property name="roleHierarchy" ref="roleHierarchy"/>
<beans:property name="dataSource" ref="dataSource"/>
<beans:property name="mapClass" value="egovframework.rte.fdl.security.userdetails.EgovUserDetailsMapping"/>
</beans:bean>
<beans:bean id="securedObjectService" class="egovframework.rte.fdl.security.securedobject.impl.SecuredObjectServiceImpl">
<beans:property name="securedObjectDAO" ref="securedObjectDAO"/>
<beans:property name="requestMatcherType" value="regex"/> <!-- default : ant -->
</beans:bean>
<beans:bean id="securedObjectDAO" class="egovframework.rte.fdl.security.securedobject.impl.SecuredObjectDAO" >
<beans:property name="dataSource" ref="dataSource"/>
</beans:bean>
아래의 속성쿼리는 SecuredObjectDAO 빈에 기본으로 내장되었으며 DBMS 벤더에 따른 SQL문 차이 또는 DB 스키마 차이로 인한 변경된 쿼리를 직접 반영할 수 있다. 내장된 기본 쿼리는 다음과 같다.
SELECT a.child_role child, a.parent_role parent
FROM ROLES_HIERARCHY a LEFT JOIN ROLES_HIERARCHY b on (a.child_role = b.parent_role)
SELECT a.resource_pattern url, b.authority authority
FROM SECURED_RESOURCES a, SECURED_RESOURCES_ROLE b
WHERE a.resource_id = b.resource_id
AND a.resource_type = 'url' ORDER BY a.sort_order
SELECT a.resource_pattern method, b.authority authority
FROM SECURED_RESOURCES a, SECURED_RESOURCES_ROLE b
WHERE a.resource_id = b.resource_id
AND a.resource_type = 'method' ORDER BY a.sort_order
SELECT a.resource_pattern pointcut, b.authority authority
FROM SECURED_RESOURCES a, SECURED_RESOURCES_ROLE b
WHERE a.resource_id = b.resource_id
AND a.resource_type = 'pointcut' ORDER BY a.sort_order
SecuredObjectDAO 빈에 내장된 SQL을 사용하지 않을 경우 아래와 같이 SQL을 지정하여 설정한다.
<beans:bean id="securedObjectDAO" class="egovframework.rte.fdl.security.securedobject.impl.SecuredObjectDAO" >
<beans:property name="dataSource" ref="dataSource"/>
<beans:property name="sqlHierarchicalRoles">
<beans:value>
SELECT a.child_role child, a.parent_role parent
FROM ROLES_HIERARCHY a LEFT JOIN ROLES_HIERARCHY b on (a.child_role = b.parent_role)
</beans:value>
</beans:property>
<beans:property name="sqlRolesAndUrl">
<beans:value>
SELECT a.resource_pattern url, b.authority authority
FROM SECURED_RESOURCES a, SECURED_RESOURCES_ROLE b
WHERE a.resource_id = b.resource_id
AND a.resource_type = 'url' ORDER BY a.sort_order
</beans:value>
</beans:property>
<beans:property name="sqlRolesAndMethod">
<beans:value>
SELECT a.resource_pattern method, b.authority authority
FROM SECURED_RESOURCES a, SECURED_RESOURCES_ROLE b
WHERE a.resource_id = b.resource_id
AND a.resource_type = 'method' ORDER BY a.sort_order
</beans:value>
</beans:property>
<beans:property name="sqlRolesAndPointcut">
<beans:value>
SELECT a.resource_pattern pointcut, b.authority authority
FROM SECURED_RESOURCES a, SECURED_RESOURCES_ROLE b
WHERE a.resource_id = b.resource_id
AND a.resource_type = 'pointcut' ORDER BY a.sort_order
</beans:value>
</beans:property>
</beans:bean>
List<String> authorities = EgovUserDetailsHelper.getAuthorities();
// 1. authorites 에 권한이 있는지 체크 TRUE/FALSE
assertTrue(authorities.contains("ROLE_USER"));
assertTrue(authorities.contains("ROLE_RESTRICTED"));
assertTrue(authorities.contains("IS_AUTHENTICATED_ANONYMOUSLY"));
assertTrue(authorities.contains("IS_AUTHENTICATED_FULLY"));
assertTrue(authorities.contains("IS_AUTHENTICATED_REMEMBERED"));
// 2. authorites 에 ROLE 이 여러개 설정된 경우
for (Iterator<String> it = authorities.iterator(); it.hasNext();) {
String auth = it.next();
}
// 3. authorites 에 ROLE 이 하나만 설정된 경우
String auth = (String) authorities.toArray()[0];
표준프레임워크 3.0부터 Server security에 대하여 설정을 간소화 할 수 있는 방법을 제공한다. 내부적으로 필요한 설정을 가지고 있고, XML Schema를 통해 필요한 설정만을 추가할 수 있도록 제공한다.
설정 간소화 기능을 사용하기 위해서는 다음과 같은 xml 선언이 필요하다. 4.1 > 4.2 업그레이드 시 xsd 변경(egov-security-4.1.0.xsd > egov-security-4.2.0.xsd)
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:egov-security="http://maven.egovframe.go.kr/schema/egov-security"
xmlns:security="http://www.springframework.org/schema/security"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security.xsd
http://maven.egovframe.go.kr/schema/egov-security http://maven.egovframe.go.kr/schema/egov-security/egov-security-4.2.0.xsd">
Security에 대한 기본 설정 정보를 제공한다.
예:
<egov-security:config id="securityConfig"
loginUrl="/uat/uia/egovLoginUsr.do"
logoutSuccessUrl="/EgovContent.do"
loginFailureUrl="/uat/uia/egovLoginUsr.do?login_error=1"
accessDeniedUrl="/sec/ram/accessDenied.do"
dataSource="egov.dataSource"
jdbcUsersByUsernameQuery="SELECT USER_ID, ESNTL_ID AS PASSWORD, 1 ENABLED, USER_NM, USER_ZIP,
USER_ADRES, USER_EMAIL, USER_SE, ORGNZT_ID, ESNTL_ID,
(select a.ORGNZT_NM from COMTNORGNZTINFO a where a.ORGNZT_ID = m.ORGNZT_ID) ORGNZT_NM
FROM COMVNUSERMASTER m WHERE CONCAT(USER_SE, USER_ID) = ?"
jdbcAuthoritiesByUsernameQuery="SELECT A.SCRTY_DTRMN_TRGET_ID USER_ID, A.AUTHOR_CODE AUTHORITY
FROM COMTNEMPLYRSCRTYESTBS A, COMVNUSERMASTER B
WHERE A.SCRTY_DTRMN_TRGET_ID = B.ESNTL_ID AND B.USER_ID = ?"
jdbcMapClass="egovframework.com.sec.security.common.EgovSessionMapping"
requestMatcherType="regex"
hash="plaintext"
hashBase64="false"
concurrentMaxSessons="1"
concurrentExpiredUrl="/EgovContent.do"
errorIfMaximumExceeded="false"
defaultTargetUrl="/EgovContent.do"
alwaysUseDefaultTargetUrl="true"
sniff="true"
xframeOptions="SAMEORIGIN"
xssProtection="true"
cacheControl="false"
csrf="false"
csrfAccessDeniedUrl="/egovCSRFAccessDenied.do"
/>
| 속성 | 설명 | 필수여부 | 비고 |
|---|---|---|---|
| loginUrl | 로그인 페이지 URL | 필수 | |
| logoutSuccessUrl | 로그아웃 처리 시 호출되는 페이지 URL | 필수 | |
| loginFailureUrl | 로그인 실패 시 호출되는 페이지 URL | 필수 | |
| accessDeniedUrl | 권한이 없는 경우 호출되는 페이지 URL | 필수 | |
| dataSource | DBMS 설정 dataSource | 선택 | 미지정시 ‘dataSource’ bean name 사용 |
| jdbcUsersByUsernameQuery | 인증에 사용되는 query | 선택 | default : “select user_id, password, enabled, users.* from users where user_id = ?” |
| jdbcAuthoritiesByUsernameQuery | 인증된 사용자의 권한(authority) 조회 query | 선택 | default : “select user_id, authority from authorites where user_id = ?” |
| jdbcMapClass | 사용자 정보 mapping 처리 class | 선택 | default : egovframework.rte.fdl.security.userdetails.DefaultMapUserDetailsMapping |
| requestMatcherType | 패턴 매칭 방식(regex, ant, ciRegex: case-insensitive regex) | 선택 | default : regex |
| hash | 패스워드 저장 방식 (sha-256, plaintext, sha, md5, bcrypt) | 선택 | default : sha-256 |
| hashBase64 | hash값 base64 인코딩 사용 여부 | 선택 | default : true |
| concurrentMaxSessons | 동시 접속가능 연결 수 | 선택 | default : 999 |
| concurrentExpiredUrl | expired된 경우 redirect되는 페이지 URL | 선택 | |
| errorIfMaximumExceeded | 중복 로그인 방지 옵션 | 필수 | default : false |
| defaultTargetUrl | 로그인 성공시 redirect되는 페이지 URL | 선택 | 미지정시 처음 접속하고자 했던 페이지 URL로 redirect됨 |
| alwaysUseDefaultTargetUrl | 로그인 이후 설정한 페이지로 이동하게 하는 옵션 | 필수 | default : true |
| sniff | 선언된 콘텐츠 유형으로부터 벗어난 응답에 대한 브라우저의 MIME 가로채기를 방지 여부 | 필수 | default : true |
| xframeOptions | sniff 옵션 이 ture 일때 X-Frame-Options 범위설정 | 선택 | DENY, SAMEORIGIN |
| xssProtection | XSS Protection 기능의 사용 여부 | 필수 | default : true |
| cacheControl | 캐쉬 비활성화 여부 옵션 | 필수 | default : false |
| csrf | spring security의 csrf 기능 사용 여부 | 필수 | default : false |
| csrfAccessDeniedUrl | 토큰 검증이 실패했을 경우 호출되는 페이지 URL | 필수 | |
| useExpressions | Spring 표현 언어(SpEL) 설정 옵션 | 선택 | default : false |
Security에 대한 초기화 처리 정보를 제공한다.
예:
<egov-security:initializer
id="initializer"
supportPointcut="true"
/>
| 속성 | 설명 | 필수여부 | 비고 |
|---|---|---|---|
| supportPointcut | pointcut 방식 지원 여부 | 선택 | default : false |
| supportMethod | method 방식 지원 여부 | 선택 | default : true |
Security에 대한 기본 query 설정 정보를 제공한다.
예:
<egov-security:secured-object-config
id="securedObjectConfig"
roleHierarchyString="
ROLE_ADMIN > ROLE_USER
ROLE_USER > ROLE_RESTRICTED
ROLE_RESTRICTED > IS_AUTHENTICATED_FULLY
IS_AUTHENTICATED_FULLY > IS_AUTHENTICATED_REMEMBERED
IS_AUTHENTICATED_REMEMBERED > IS_AUTHENTICATED_ANONYMOUSLY"
sqlRolesAndUrl="
SELECT auth.URL url, code.CODE_NM authority
FROM RTETNAUTH auth, RTETCCODE code
WHERE code.CODE_ID = auth.MNGR_SE"
/>
| 속성 | 설명 | 필수여부 | 비고 |
|---|---|---|---|
| roleHierarchyString | 계층처리를 위한 설정 문자열 지정 | 선택 | 미지정시 DB로부터 지정된 설정정보 지정 |
| sqlRolesAndUrl | URL 방식 role 지정 query | 선택 | 미지정시 SecuredObjectDAO의 기본 query가 처리됨 |
| sqlRolesAndMethod | method 방식 role 지정 query | 선택 | 〃 |
| sqlRolesAndPointcut | pointcut 방식 role 지정 query | 선택 | 〃 |
| sqlRegexMatchedRequestMapping | request 마다 best matching url 보호자원 지정 query | 선택 | 〃 |
| sqlHierarchicalRoles | 계층처리를 위한 query | 선택 | 〃 |
표준프레임워크 2.7(Spring Security 2.0.4)에서 3.0(Spring Security 3.2.3)로 업그레이드 Server security의 경우 설정 변경뿐만 아니라 소스 상의 변경 작업이 필요하다.
<dependency>
<groupId>egovframework.rte</groupId>
<artifactId>egovframework.rte.fdl.security</artifactId>
<version>3.0.0</version>
</dependency>
접속 제한을 사용하는 경우 web.xml 상에 HttpSessionEventPublisher listener의 패키지 변경 필요
<listener>
<listener-class>org.springframework.security.ui.session.HttpSessionEventPublisher</listener-class>
</listener>
<listener>
<listener-class>org.springframework.security.web.session.HttpSessionEventPublisher</listener-class>
</listener>
다음 설정을 참조하여 관련 설정을 변경한다. (Spring Security쪽 패키지 등)
<beans:beans xmlns="http://www.springframework.org/schema/security"
xmlns:beans="http://www.springframework.org/schema/beans"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.2.xsd
http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-3.2.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.2.xsd">
<beans:bean id="securedObjectService" class="egovframework.rte.fdl.security.securedobject.impl.SecuredObjectServiceImpl">
<beans:property name="securedObjectDAO" ref="securedObjectDAO"/>
<beans:property name="requestMatcherType" value="regex"/> <!-- default : ant -->
</beans:bean>
<beans:bean id="securedObjectDAO" class="egovframework.rte.fdl.security.securedobject.impl.SecuredObjectDAO" >
<beans:property name="dataSource" ref="dataSource"/>
<!--
<beans:property name="sqlHierarchicalRoles">
<beans:value>
SELECT a.child_role child, a.parent_role parent
FROM ROLES_HIERARCHY a LEFT JOIN ROLES_HIERARCHY b on (a.child_role = b.parent_role)
</beans:value>
</beans:property>
<beans:property name="sqlRolesAndUrl">
<beans:value>
SELECT a.resource_pattern url, b.authority authority
FROM SECURED_RESOURCES a, SECURED_RESOURCES_ROLE b
WHERE a.resource_id = b.resource_id
AND a.resource_type = 'url' ORDER BY a.sort_order
</beans:value>
</beans:property>
<beans:property name="sqlRolesAndMethod">
<beans:value>
SELECT a.resource_pattern method, b.authority authority
FROM SECURED_RESOURCES a, SECURED_RESOURCES_ROLE b
WHERE a.resource_id = b.resource_id
AND a.resource_type = 'method' ORDER BY a.sort_order
</beans:value>
</beans:property>
<beans:property name="sqlRolesAndPointcut">
<beans:value>
SELECT a.resource_pattern pointcut, b.authority authority
FROM SECURED_RESOURCES a, SECURED_RESOURCES_ROLE b
WHERE a.resource_id = b.resource_id
AND a.resource_type = 'pointcut' ORDER BY a.sort_order
</beans:value>
</beans:property>
-->
</beans:bean>
<!-- 불필요 삭제 -->
<!--
<beans:bean id="userDetailsServiceWrapper" class="org.springframework.security.userdetails.hierarchicalroles.UserDetailsServiceWrapper">
<beans:property name="roleHierarchy" ref="roleHierarchy"/>
<beans:property name="userDetailsService" ref="jdbcUserService"/>
</beans:bean>
-->
<beans:bean id="roleHierarchy" class="org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl" >
<!-- XML 사용
<beans:property name="hierarchy">
<beans:value>
ROLE_ADMIN > ROLE_USER
ROLE_USER > ROLE_RESTRICTED
ROLE_RESTRICTED > IS_AUTHENTICATED_FULLY
IS_AUTHENTICATED_REMEMBERED > IS_AUTHENTICATED_ANONYMOUSLY
</beans:value>
</beans:property>
-->
<!-- DB 사용 -->
<beans:property name="hierarchy" ref="hierarchyStrings"/>
</beans:bean>
<beans:bean id="hierarchyStrings" class="egovframework.rte.fdl.security.userdetails.hierarchicalroles.HierarchyStringsFactoryBean" init-method="init">
<beans:property name="securedObjectService" ref="securedObjectService"/>
</beans:bean>
<!--
Access Decision Manager는 자동으로 생성되기 때문에 선언 불필요
bean id : org.springframework.security.access.vote.AffirmativeBased#0
※ #0 부분은 숫자 부분은 선언 순으로 순차적으로 생성됨
-->
<!--
<beans:bean id="accessDecisionManager" class="org.springframework.security.access.vote.AffirmativeBased">
<beans:property name="allowIfAllAbstainDecisions" value="false" />
<beans:property name="decisionVoters">
<beans:list>
<beans:bean class="org.springframework.security.access.vote.RoleVoter">
<beans:property name="rolePrefix" value="ROLE_" />
</beans:bean>
<beans:bean class="org.springframework.security.access.vote.AuthenticatedVoter" />
</beans:list>
</beans:property>
</beans:bean>
-->
<beans:bean id="filterSecurityInterceptor" class="org.springframework.security.web.access.intercept.FilterSecurityInterceptor">
<beans:property name="authenticationManager" ref="org.springframework.security.authenticationManager" />
<beans:property name="accessDecisionManager" ref="org.springframework.security.access.vote.AffirmativeBased#0" />
<beans:property name="securityMetadataSource" ref="databaseSecurityMetadataSource" />
</beans:bean>
<beans:bean id="databaseSecurityMetadataSource" class="egovframework.rte.fdl.security.intercept.EgovReloadableFilterInvocationSecurityMetadataSource">
<beans:constructor-arg ref="requestMap" />
<beans:property name="securedObjectService" ref="securedObjectService"/>
</beans:bean>
<!-- url -->
<beans:bean id="requestMap" class="egovframework.rte.fdl.security.intercept.UrlResourcesMapFactoryBean" init-method="init">
<beans:property name="securedObjectService" ref="securedObjectService"/>
</beans:bean>
<!-- 지정 불필요 : request-matcher 참조 -->
<!--
<beans:bean id="regexUrlPathMatcher" class="org.springframework.security.web.util.matcher.RegexRequestMatcher" />
-->
<http pattern="/css/**" security="none"/>
<http pattern="/images/**" security="none"/>
<http pattern="/js/**" security="none"/>
<http pattern="\A/WEB-INF/jsp/.*\Z" request-matcher="regex" security="none"/>
<http access-denied-page="/system/accessDenied.do" request-matcher="regex">
<form-login login-processing-url="/j_spring_security_check"
authentication-failure-url="/cvpl/EgovCvplLogin.do?login_error=1"
default-target-url="/index.jsp?flag=L"
login-page="/cvpl/EgovCvplLogin.do" />
<anonymous/>
<logout logout-success-url="/cvpl/EgovCvplLogin.do"/>
<!-- for authorization -->
<custom-filter before="FILTER_SECURITY_INTERCEPTOR" ref="filterSecurityInterceptor"/>
</http>
<!--
authentication-manager 기본 생성 bean id : org.springframework.security.authenticationManager
(alias로 변경할 수 있음)
-->
<authentication-manager>
<authentication-provider user-service-ref="jdbcUserService">
<password-encoder hash="sha-256" base64="true"/>
</authentication-provider>
</authentication-manager>
<!-- userDetailsServiceWrapper -->
<!-- customizing user table, authorities table -->
<!--<jdbc-user-service id="jdbcUserService" data-source-ref="dataSource"
users-by-username-query="SELECT USER_ID,PASSWORD,ENABLED,BIRTH_DAY FROM USERS WHERE USER_ID = ?"
authorities-by-username-query="SELECT USER_ID,AUTHORITY FROM AUTHORITIES WHERE USER_ID = ?"/>-->
<beans:bean id="jdbcUserService" class="egovframework.rte.fdl.security.userdetails.jdbc.EgovJdbcUserDetailsManager" >
<beans:property name="usersByUsernameQuery" value="SELECT USER_ID,PASSWORD,ENABLED,USER_NAME,BIRTH_DAY,SSN FROM USERS WHERE USER_ID = ?"/>
<beans:property name="authoritiesByUsernameQuery" value="SELECT USER_ID,AUTHORITY FROM AUTHORITIES WHERE USER_ID = ?"/>
<beans:property name="roleHierarchy" ref="roleHierarchy"/>
<beans:property name="dataSource" ref="dataSource"/>
<beans:property name="mapClass" value="egovframework.rte.fdl.security.userdetails.EgovUserDetailsMapping"/>
</beans:bean>
<!-- method -->
<beans:bean id="methodSecurityMetadataSourceAdvisor" class="org.springframework.security.access.intercept.aopalliance.MethodSecurityMetadataSourceAdvisor">
<beans:constructor-arg value="methodSecurityInterceptor" />
<beans:constructor-arg ref="delegatingMethodSecurityMetadataSource" />
<beans:constructor-arg value="delegatingMethodSecurityMetadataSource" />
</beans:bean>
<beans:bean id="methodSecurityInterceptor" class="org.springframework.security.access.intercept.aopalliance.MethodSecurityInterceptor">
<beans:property name="validateConfigAttributes" value="false" />
<beans:property name="authenticationManager" ref="org.springframework.security.authenticationManager"/>
<beans:property name="accessDecisionManager" ref="org.springframework.security.access.vote.AffirmativeBased#0"/>
<beans:property name="securityMetadataSource" ref="delegatingMethodSecurityMetadataSource" />
</beans:bean>
<beans:bean id="delegatingMethodSecurityMetadataSource" class="org.springframework.security.access.method.DelegatingMethodSecurityMetadataSource">
<beans:constructor-arg>
<beans:list>
<beans:ref bean="methodSecurityMetadataSources" />
<beans:bean class="org.springframework.security.access.annotation.SecuredAnnotationSecurityMetadataSource" />
<beans:bean class="org.springframework.security.access.annotation.Jsr250MethodSecurityMetadataSource" />
</beans:list>
</beans:constructor-arg>
</beans:bean>
<beans:bean id="methodSecurityMetadataSources" class="org.springframework.security.access.method.MapBasedMethodSecurityMetadataSource">
<beans:constructor-arg ref="methodMap" />
</beans:bean>
<beans:bean id="methodMap" class="egovframework.rte.fdl.security.intercept.MethodResourcesMapFactoryBean" init-method="init">
<beans:property name="securedObjectService" ref="securedObjectService"/>
<beans:property name="resourceType" value="method"/>
</beans:bean>
<!-- pointcut -->
<!-- if no map, there is a error that "this map must not be empty; it must contain at least one entry" -->
<!-- // so there is dummy entry
<beans:bean id="protectPointcutPostProcessor" class="org.springframework.security.config.method.ProtectPointcutPostProcessor">
<beans:constructor-arg ref="methodSecurityMetadataSources" />
<beans:property name="pointcutMap" ref="pointcutMap"/>
</beans:bean>
<beans:bean id="pointcutMap" class="egovframework.rte.fdl.security.intercept.MethodResourcesMapFactoryBean" init-method="init">
<beans:property name="securedObjectService" ref="securedObjectService"/>
<beans:property name="resourceType" value="pointcut"/>
</beans:bean>
-->
</beans:beans>
자체 적용된 server security에 대한 소스 삭제 정리 Ex:
jdbcUserService(egovframework.rte.fdl.security.userdetails.jdbc.EgovJdbcUserDetailsManager)에 의해 지정된 mapClass는 EgovUsersByUsernameMapping 클래스를 extend 하도록 되어 있는데, 해당 EgovUsersByUsernameMapping의 mapRow() 메소드의 return 타입이 Object에서 EgovUserDtails로 변경되었다.
public class EgovSessionMapping extends EgovUsersByUsernameMapping {
...
@Override
protected Object mapRow(ResultSet rs, int rownum) throws SQLException {
...
}
}
public class EgovSessionMapping extends EgovUsersByUsernameMapping {
...
@Override
protected EgovUserDetails mapRow(ResultSet rs, int rownum) throws SQLException {
...
}
}
Spring security 관련 패키지 변경 등에 따라 일부 참조 클래스에 대한 패키지 변경 필요
GrantedAuthority[] → Collection<GrantedAuthority> 변경 적용
GrantedAuthority[] authorities = authentication.getAuthorities();
for (int i = 0; i < authorities.length; i++) {
listAuth.add(authorities[i].getAuthority());
}
Collection<GrantedAuthority> authorities = (Collection<GrantedAuthority>) authentication.getAuthorities();
for (GrantedAuthority authority : authorities) {
listAuth.add(authority.getAuthority());
}
기존의 경우 로그인되지 않은 경우 SecurityContext의 getAuthentication()의 리턴 값이 null이었으나,
SecurityContext context = SecurityContextHolder.getContext();
Authentication authentication = context.getAuthentication();
if (EgovObjectUtil.isNull(authentication)) {
return null;
}
신규 버전의 경우는 null이 아닌 값으로 처리된다.
SecurityContext context = SecurityContextHolder.getContext();
Authentication authentication = context.getAuthentication();
if (EgovObjectUtil.isNull(authentication)) {
log.debug("## authentication object is null!!");
return null;
}
if (authentication.getPrincipal() instanceof EgovUserDetails) {
EgovUserDetails details = (EgovUserDetails) authentication.getPrincipal();
log.debug("## EgovUserDetailsHelper.getAuthenticatedUser : AuthenticatedUser is {}", details.getUsername());
return details.getEgovUserVO();
} else {
return authentication.getPrincipal();
}
따라서 사용자 정보를 취득하는 부분은 자체 적용 부분이 아닌 실행환경 제공 부분(egovframework.rte.fdl.security.userdetails.util.EgovUserDetailsHelper)을 사용한다.
신규 버전의 경우는 GET 방식으로 j_spring_security_check를 호출할 수 없게 되었다.(j_spring_security_check URL을 내부적으로 redirect 호출하는 경우에만 해당) 오류 메시지 : Authentication request failed: org.springframework.security.authentication.AuthenticationServiceException: Authentication method not supported: GET
이 경우 다음 코드와 같이 GET 방식이 아닌 filter chain 호출 방식으로 변경해주어야 한다
return "redirect:/j_spring_security_check?j_username=" + resultVO.getUserSe() + resultVO.getId() + "&j_password=" + resultVO.getUniqId();
@RequestMapping(value="/uat/uia/actionSecurityLogin.do")
public String actionSecurityLogin(@ModelAttribute("loginVO") LoginVO loginVO,
HttpServletRequest request, HttpServletResponse response,
ModelMap model)
throws Exception {
...
UsernamePasswordAuthenticationFilter springSecurity = null;
ApplicationContext act = WebApplicationContextUtils.getRequiredWebApplicationContext(request.getSession().getServletContext());
@SuppressWarnings("rawtypes")
Map beans = act.getBeansOfType(UsernamePasswordAuthenticationFilter.class);
if (beans.size() > 0) {
springSecurity = (UsernamePasswordAuthenticationFilter)beans.values().toArray()[0];
} else {
throw new IllegalStateException("No AuthenticationProcessingFilter");
}
springSecurity.setContinueChainBeforeSuccessfulAuthentication(false); // false 이면 chain 처리 되지 않음.. (filter가 아닌 경우 false로...)
springSecurity.doFilter(
new RequestWrapperForSecurity(request, resultVO.getUserSe() + resultVO.getId() , resultVO.getUniqId()),
response, null);
return "forward:/cmm/main/mainPage.do"; // 성공 시 페이지.. (redirect 불가)
...
}
...
class RequestWrapperForSecurity extends HttpServletRequestWrapper {
private String username = null;
private String password = null;
public RequestWrapperForSecurity(HttpServletRequest request, String username, String password) {
super(request);
this.username = username;
this.password = password;
}
@Override
public String getRequestURI() {
return ((HttpServletRequest)super.getRequest()).getContextPath() + "/j_spring_security_check";
}
@Override
public String getParameter(String name) {
if (name.equals("j_username")) {
return username;
}
if (name.equals("j_password")) {
return password;
}
return super.getParameter(name);
}
}
public class EgovSpringSecurityLoginFilter implements Filter{
private FilterConfig config;
public void init(FilterConfig filterConfig) throws ServletException {
this.config = filterConfig;
}
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
...
HttpServletRequest httpRequest = (HttpServletRequest)request;
HttpServletResponse httpResponse = (HttpServletResponse)response;
...
UsernamePasswordAuthenticationFilter springSecurity = null;
ApplicationContext act = WebApplicationContextUtils.getRequiredWebApplicationContext(config.getServletContext());
@SuppressWarnings("rawtypes")
Map beans = act.getBeansOfType(UsernamePasswordAuthenticationFilter.class);
if (beans.size() > 0) {
springSecurity = (UsernamePasswordAuthenticationFilter)beans.values().toArray()[0];
} else {
throw new IllegalStateException("No AuthenticationProcessingFilter");
}
//springSecurity.setContinueChainBeforeSuccessfulAuthentication(false); // false 이면 chain 처리 되지 않음.. (filter가 아닌 경우 false로...)
springSecurity.doFilter(
new RequestWrapperForSecurity(request, resultVO.getUserSe() + resultVO.getId() , resultVO.getUniqId()),
response, chain);
...
}
...
}
class RequestWrapperForSecurity extends HttpServletRequestWrapper {
private String username = null;
private String password = null;
public RequestWrapperForSecurity(HttpServletRequest request, String username, String password) {
super(request);
this.username = username;
this.password = password;
}
@Override
public String getRequestURI() {
return ((HttpServletRequest)super.getRequest()).getContextPath() + "/j_spring_security_check";
}
@Override
public String getParameter(String name) {
if (name.equals("j_username")) {
return username;
}
if (name.equals("j_password")) {
return password;
}
return super.getParameter(name);
}
}
AuthorityResourceMetadata의 reload() 메소드를 호출하여 설정을 적용할 수 있다.표준프레임워크 3.9부터 Session 방식으로 접근제어 권한관리를 설정 할 수 있는 방법을 제공한다. 내부적으로 필요한 설정을 가지고 있고, XML Schema를 통해 필요한 설정만을 추가할 수 있도록 제공한다. 이 기능을 사용하기 위해서는 globals.properties 파일에서 Globals.Auth = session 로 설정한다.
Session 방식의 접근제어 권한관리를 사용하기 위해서는 표준프레임워크 실행환경 구성요소중 org.egovframe.rte.fdl.access 라이브러리가 설치되어야 한다.
<dependency>
<groupId>org.egovframe.rte</groupId>
<artifactId>org.egovframe.rte.fdl.access</artifactId>
<version>${org.egovframe.rte.version}</version>
</dependency>
접근제어를 설정하기 위해서는 다음과 같은 xml 선언이 필요하다.
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:egov-access="http://maven.egovframe.go.kr/schema/egov-access"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://maven.egovframe.go.kr/schema/egov-access http://maven.egovframe.go.kr/schema/egov-access/egov-access-4.2.0.xsd">
Session 방식 접근제어 권한관리에 대한 기본 설정 정보를 제공한다.
<egov-access:config id="egovAccessConfig"
globalAuthen="session"
mappingPath="/**/*.do"
dataSource="egov.dataSource"
loginUrl="/uat/uia/egovLoginUsr.do"
accessDeniedUrl="/uat/uia/egovLoginUsr.do?auth_error=1"
sqlAuthorityUser="SELECT CONCAT(B.USER_SE, B.USER_ID) USERID, A.AUTHOR_CODE AUTHORITY
FROM COMTNEMPLYRSCRTYESTBS A, COMVNUSERMASTER B
WHERE A.SCRTY_DTRMN_TRGET_ID = B.ESNTL_ID"
sqlRoleAndUrl="SELECT A.ROLE_PTTRN URL, B.AUTHOR_CODE AUTHORITY
FROM COMTNROLEINFO A, COMTNAUTHORROLERELATE B
WHERE A.ROLE_CODE = B.ROLE_CODE
AND A.ROLE_TY = 'url'
ORDER BY A.ROLE_SORT"
requestMatcherType="regex"
excludeList="/uat/uia/**, /index.do, /EgovLeft.do, /EgovContent.do, /EgovTop.do, /EgovBottom.do, /validator.do, /uss/umt/**, /sec/rnc/EgovRlnmCnfirm.do, /EgovModal.do"
/>
| 속성 | 설명 | 필수여부 | 비고 |
|---|---|---|---|
| globalAuthen | globals.properties 설정과 동일하게 적용 (Globals.Auth = session 설정 사용시 globalAuthen = “session”으로 값을 동일하게 일치하여 설정 필요) | 필수 | |
| dataSource | DBMS 설정 dataSource | 필수 | |
| loginUrl | 로그인 페이지 URL | 필수 | |
| accessDeniedUrl | 권한이 없는 경우 호출되는 페이지 URL | 필수 | |
| sqlAuthorityUser | 인증된 사용자의 권한(authority) 조회 query | 필수 | |
| sqlRoleAndUrl | Role 및 URL 패턴 | 필수 | |
| requestMatcherType | 패턴 매칭 방식(regex, ant, ciRegex: case-insensitive regex) | 필수 | default : regex |
| excludeList | 접근제한 예외처리 URL(구분자: ,) | 필수 |
<c:import> 혹은 <jsp:include> 사용시 호출대상 URL이 .do 인 경우Session 접근제어에서 사용자의 롤권한변경 후 서버 재기동 없이 적용하는 방법을 제공한다.
import org.egovframe.rte.fdl.access.bean.AuthorityResourceMetadata;
@Resource(name="authorityResource")
private AuthorityResourceMetadata sessionResourceMetadata;
@RequestMapping(value="/insertAuthorGroupInsert.do")
public String insertAuthorGroup() {
...
sessionResourceMetadata.reload();
...
}
Scheduling 서비스는 어플리케이션 서버 내에서 주기적으로 발생하거나 반복적으로 발생하는 작업을 지원하는 기능으로서 유닉스의 크론(Cron) 명령어와 유사한 기능을 제공한다.
실행환경 Scheduling 서비스는 오픈소스 소프트웨어로 Quartz 스케쥴러를 사용한다. 본 장에서는 Quartz 스케쥴러의 기본 개념을 살펴본 후, IoC 서비스를 제공하는 Spring과 Quartz 스케쥴러를 통합하여 사용하는 방법을 살펴본다.
Quartz 스케쥴러 실행과 관계된 주요 요소는 Scheduler, Job, JobDetail, Trigger 가 있다.
Quartz 스케쥴러는 수행 작업을 정의하는 Job과 실행 스케쥴을 정의하는 Trigger를 분리함으로써 유연성을 제공한다. Job 과 실행 스케쥴을 정의한 경우, Job은 그대로 두고 실행 스케쥴만을 변경할 수 있다. 또한 하나의 Job에 여러 개의 실행 스케쥴을 정의할 수 있다.
Quartz 스케쥴러의 이해를 돕기 위해 간단한 예제를 살펴본다. 다음 예는 Quartz 매뉴얼에서 참조한 것으로 Quartz를 사용하는 방법과 사용자 Job을 설정하는 방법을 보여준다.
사용자는 Job 개체를 생성하기 위해 org.quartz.Job 인터페이스를 구현하고 심각한 오류가 발생한 경우 JobExecutionException 예외를 던질 수 있다. Job 인터페이스는 단일 메소드로 execute()을 정의한다.
public class DumbJob implements Job {
public void execute(JobExecutionContext context) throws JobExecutionException {
System.out.println("DumbJob is executing.");
}
}
JobDetail jobDetail =
new JobDetail("myJob",// Job 명
sched.DEFAULT_GROUP, // Job 그룹명('null' 값인 경우 DEFAULT_GROUP 으로 정의됨)
DumbJob.class); // 실행할 Job 클래스
Trigger trigger = TriggerUtils.makeDailyTrigger(8, 30); // 매일 08시 30분 실행
trigger.setStartTime(new Date()); // 즉시 시작
trigger.setName("myTrigger");
sched.scheduleJob(jobDetail, trigger);
Spring은 Scheduling 지원을 위한 통합 클래스를 제공한다. Spring Framework는 JDK 1.3 버전부터 포함된 Timer 와 오픈소스 소프트웨어인 Quartz 스케쥴러를 지원한다. 여기서는 Quartz 스케쥴러와 Spring을 통합하여 사용하는 방법을 살펴본다.
Quartz 스케쥴러와의 통합을 위해 Spring은 Spring 컨텍스트 내에서 Quart Scheduler와 JobDetail, Trigger 를 빈으로 설정할 수 있도록 지원한다. 다음은 예제를 중심으로 Quartz 작업 생성과 작업 스케쥴링, 작업 시작 방법을 살펴본다.
Spring은 작업 생성을 위한 방법으로 다음 두 가지 방식을 제공한다.
JobDetail는 작업 실행에 필요한 정보를 담고 있는 객체이다. Spring은 JobDetail 빈 생성을 위해 JobDetailBean을 제공한다. 예를 들면 다음과 같다.
package egovframework.rte.fdl.scheduling.sample;
public class SayHelloJob extends QuartzJobBean {
private String name;
public void setName (String name) {
this.name = name;
}
@Override
protected void executeInternal (JobExecutionContext ctx) throws JobExecutionException {
System.out.println("Hello, " + name);
}
}
<bean id="jobDetailBean"
class="org.springframework.scheduling.quartz.JobDetailBean">
<property name="jobClass" value="egovframework.rte.fdl.scheduling.sample.SayHelloJob" />
<property name="jobDataAsMap">
<map>
<entry key="name" value="JobDetail"/>
</map>
</property>
</bean>
package egovframework.rte.fdl.scheduling.sample;
public class SayHelloService {
private String name;
public void setName (String name) {
this.name = name;
}
public void sayHello () {
System.out.println("Hello, " + this.name);
}
}
<bean id="sayHelloService" class="egovframework.rte.fdl.scheduling.sample.SayHelloService">
<property name="name" value="FactoryBean"/>
</bean>
<bean id="jobDetailFactoryBean" class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean">
<property name="targetObject" ref="sayHelloService" />
<property name="targetMethod" value="sayHello" />
<property name="concurrent" value="false" />
</bean>
Spring에서 주로 사용되는 Trigger타입은 SimpleTriggerBean과 CronTriggerBean 이 있다. SimpleTrigger 는 특정 시간, 반복 회수, 대기 시간과 같은 단순 스케쥴링에 사용된다. CronTrigger 는 유닉스의 Cron 명령어와 유사하며, 복잡한 스케쥴링에 사용된다. CronTrigger 는 달력을 이용하듯 특정 시간, 요일, 월에 Job 을 수행하도록 설정할 수 있다. 다음은 SimpleTriggerBean과 CronTriggerBean을 이용하여 앞서 생성한 작업을 스케쥴링하는 방법을 살펴본다.
<bean id="simpleTrigger" class="org.springframework.scheduling.quartz.SimpleTriggerBean">
<property name="jobDetail" ref="jobDetailBean" />
<!-- 즉시 시작 -->
<property name="startDelay" value="0" />
<!-- 매 10초마다 실행 -->
<property name="repeatInterval" value="10000" />
</bean>
<bean id="cronTrigger" class="org.springframework.scheduling.quartz.CronTriggerBean">
<property name="jobDetail" ref="jobDetailFactoryBean" />
<!-- 매 10초마다 실행 -->
<property name="cronExpression" value="*/10 * * * * ?" />
</bean>
스케쥴링한 작업의 시작을 위해 Spring 은 SchedulerFactoryBean을 제공한다.
<bean id="scheduler" class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
<property name="triggers">
<list>
<ref bean="simpleTrigger" />
<ref bean="cronTrigger" />
</list>
</property>
</bean>
전자정부 표준프레임워크 3.0부터는 다양한 Logging Framework와 연계할 수 있도록 SLF4J를 도입하였고,
Logging 구현체는 Log4j 2를 이용하여 Logging을 수행한다.
Logging 서비스는 시스템의 개발이나 운용시 발생할 수 있는 사항에 대해서,
시스템의 외부 저장소에 기록하여 시스템의 상황을 쉽게 파악할 수 있도록 도와준다.
뿐만 아니라 테스팅 코드와 운영 코드를 동일하게 가져가면서 로깅을 선언적으로 관리할 수 있다.
과도한 Logging은 운영시 성능 오버헤드를 발생시킬 수 있으므로, 최소화할 수 있는 메커니즘이 필요하다.
많은 개발자가 Log을 출력하기 위해 일반적으로 사용하는 방식은 System.out.println()이다.
하지만 이 방식은 간편한 반면, 다음과 같은 이유로 권장하지 않는다.
본 페이지에서는 SLF4j와 Log4j 2에 대한 기본 사용법과 Migration에 대해 설명한다.
SLF4J(Simple Logging Facade For Java)는 특정 Logging 서비스 구현체에 종속되지 않도록 추상화 계층을 제공하며,
Jakarta Commons Logging(JCL), Log4j, Logback 등과 함께 사용할 수 있다.
다음은 SLF4J 샘플 예제이다.
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class Slf4JLoggerTest {
// SLF4J를 이용한 Logger 오브젝트 생성
private static final Logger LOGGER = LoggerFactory.getLogger(Slf4JLoggerTest.class);
// Parameterized logging - String 타입
String message = "Hello, eGovFrame 3.0";
String message2 = "Welcome to eGovFrame 3.0";
LOGGER.debug("SLF4J Logger - {}", message); // 출력결과 - SLF4J Logger - Hello, eGovFrame 3.0
LOGGER.debug("SLF4J Logger - {} and {}", message, message2); // 출력결과 - SLF4J Logger - Hello, eGovFrame 3.0 and Welcome to eGovFrame 3.0
// Parameterized logging - Object 타입
Object[] args = new Object[3];
args[0] = "1";
args[1] = Integer.valueOf("2");
args[2] = new Date().toString();
LOGGER.debug("SLF4J Logger - {}, {}, {}", args); // 출력결과 - SLF4J Logger - 1, 2, Fri Mar 23 11:08:28 KST 2014
}
<!-- SLF4J -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>x.x.x</version>
</dependency>
<!-- Exclude Commons Logging in favor of SLF4J -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${spring.maven.artifact.version}</version>
<exclusions>
<exclusion>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- SLF4J JCL Bridge -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>jcl-over-slf4j</artifactId>
<version>x.x.x</version>
</dependency>
| Logging 구현체 | SLF4J Binding jar |
|---|---|
| Log4j 2 | log4j-slf4j-impl.jar |
| Log4j 1.2 | slf4j-log4j12.jar |
| JDK 1.x Logging | slf4j-jdk14.jar |
| NOP | slf4j-nop.jar |
| JCL | slf4j-jcl.jar |
| Logback | logback-classic.jar, logback-core.jar |
<!-- SLF4J Log4j1.2 Binding -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>x.x.x</version>
</dependency>
<!-- Log4j 1.2 -->
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2</version>
</dependency>
<!-- Log4j2 SLF4J Binding -->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-slf4j-impl</artifactId>
<version>x.x.x</version>
</dependency>
<!-- Log4j 2 -->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
<version>x.x.x</version>
</dependency>
private static final Logger LOGGER = LoggerFactory.getLogger(Slf4JLoggerTest.class);
// {}-placeholder를 이용한 Parameterized Logging
String message = "Hello, eGovFrame 3.0";
LOGGER.debug("SLF4J Logger - {}", message);
기존 Legacy API을 유지한 채 SLF4J를 함께 사용하려면, SLF4J와 레거시 API를 연결할 수 있는 Bridge jar가 필요하다.
아래에서는 Log4j 1.x와 JCL 레거시를 기준으로 설명한다.
이는 각 구현체의 Logging 제어권을 SLF4J로 넘긴다는 것을 의미하며, 레거시 API를 유지하기 위해서 필요한 작업이다.
<!-- Log4j 1.x -->
<!--
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>x.x.x</version>
</dependency>
-->
<!-- SLF4J Log4j 1.x Bridge -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>log4j-over-slf4j</artifactId>
<version>x.x.x</version>
</dependency>
(주의) log4j-over-slf4j.jar는 slf4j-log4j12.jar(SLF4j Binding)과 동시에 사용할 수 없다.
<!-- Commons Logging -->
<!--
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>1.1.1</version>
</dependency>
-->
<!-- SLF4j JCL Bridge -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>jcl-over-slf4j</artifactId>
<version>x.x.x</version>
</dependency>
log4j 환경설정 파일은 SLF4J가 인식할 수 없기 때문에, 기존 환경설정을 logback으로 변경해야한다.
log4j properties file translator 를 이용하거나 logback manual 을 참조하여 변경할 수 있다.
다음은 logback.xml 샘플이다.
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<!-- encoders are assigned the type
ch.qos.logback.classic.encoder.PatternLayoutEncoder by default -->
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<root level="debug">
<appender-ref ref="STDOUT" />
</root>
</configuration>
로그 메시지를 구성하는 방법으로, 기존 문자열 결합 방식과 달리 {} 안에 파라미터를 대입하여 로그 메시지를 생성하는 방법이다.
아래 코드는 출력할 로그 메시지를 완성하기 전에 Log Level을 체크하고, isDebugEnabled인 경우에만 메서드를 수행한다.
logger.debug("Logging in user {} with birthday {}", user.getName(), user.getBirthdayCalendar());
Substituting Parameters 로깅 방식을 사용하면 코드 내에서 직접 formatting이 가능하다.
이 기능을 사용하려면 getFormatterLogger() 메서드를 통해 Logger 오브젝트를 생성해야 한다.
포맷 변환 문자와 형식은 Java.util.Formatter 클래스를 참조한다.
public static Logger logger = LogManager.getFormatterLogger("egovframework");
logger.debug("Logging in user %1$s with birthday %2$tm %2$te,%2$tY", user.getName(), user.getBirthdayCalendar());
logger.debug("Integer.MAX_VALUE = %,d", Integer.MAX_VALUE);
logger.debug("Long.MAX_VALUE = %,d", Long.MAX_VALUE);
// 출력결과
// User John Smith with birthday 05 23, 1995
// Integer.MAX_VALUE = 2,147,483,647
// Long.MAX_VALUE = 9,223,372,036,854,775,807
Log4j 2는 trace(), debug(), info() 등과 같은 로깅 메서드 뿐 아니라, 어플리케이션 실행 순서를 좀 더 쉽게 파악할 수 있도록 하는 추가적인 메서드를 제공한다.
| 메서드 | 기능 | 위치 | 사용 |
|---|---|---|---|
| entry() | 로그의 시작을 표시, 전달된 메서드 파라미터 출력 | 로깅 메서드 시작부분 | logger.entry() or logger.entry(Object… params) |
| exit() | 로그의 끝을 표시, 리턴값 출력 | return문 or 로깅 메서드 끝부분 | logger.exit() or logger.exit(Object… result) |
| throwing() | 예외나 에러가 발생했을 때, 해당 예외/에러정보를 출력 | 예외발생 시 | throw logger.throwing(new MyException); |
| catching() | 예외을 catch했을 때, 해당 예외정보를 출력 | catch문 | logger.catching(e); |
이러한 메서드들이 만드는 로깅 이벤트가 기본적인 로깅 이벤트와 분리될 수 있도록 디폴트 Log Level와 Marker가 설정되어 있다.
이에 따라 entry와 exit 메서드는 TRACE 레벨에서만 출력되며 FLOW Marker를 통해 다른 로그 메세지로부터 분리(필터링)할 수 있고,
throwing과 catching 메서드는 ERROR 레벨에서만 출력되며 EXCEPTION Marker를 통해 필터링할 수 있다.
| 메서드 | Log Level | Marker |
|---|---|---|
| entry() | TRACE | ENTER or FLOW |
| exit() | TRACE | EXIT or FLOW |
| throwing() | ERROR | THROWING or EXCEPTION |
| catching() | ERROR | CATCHING or EXCEPTION |
public String saveDept(String deptNo) {
logger.entry(deptNo); // 메서드 시작부분에 명시, 전달받은 파라미터 출력
Dept dept = service.saveDept(deptNo);
String nextPg = "redirect:/dept/deptList.do";
return logger.exit(nextPg); // 메서드 종료부분에 명시, 리턴할 파라미터 출력
}
public static void main(String[] args) {
saveDept("20");
// 출력결과
// TRACE saveDept - entry(20)
// TRACE saveDept - exit(redirect:/dept/deptList.do)
}
한꺼번에 다량의 로그가 출력되면 어느 위치에서 문제가 발생했는지 정확하게 예측할 수 없다.
또한 Log4j와 같은 Logging Framework를 사용하는 이유는 어플리케이션에서 발생하는 문제를 확인하고 디버깅 하기 위한 것이다.
이는 원하는 시점에서 로그 정보의 필터링이 가능해야함을 뜻한다.
이미 로깅 메서드와 Logger의 Log Level 설정을 통해 출력할 로그를 필터링 할 수 있지만,
Log4j 2에서는 Marker 기능을 통해 좀 더 상세한 필터링을 지원한다. 예를 들어, Flow Tracing 메서드와 기본적인 로깅 이벤트를 분리하고 싶거나, SQL문만 별도로 출력하고 싶은 경우에는 Marker 설정을 통해 기능을 구현할 수 있다.
Marker는 다음과 같은 특징을 갖는다.
public class MyClass {
private static final Logger LOGGER = LogManager.getLogger(MyClass.class);
// Marker name = "SQL"
private static final Marker SQL_MARKER = MarkerManager.getMarker("SQL");
// Marker name = "SQL_UPDATE", Parent marker = SQL_MARKER
private static final Marker UPDATE_MARKER = MarkerManager.getMarker("SQL_UPDATE", SQL_MARKER);
// Marker name = "SQL_QUERY", Parent marker = SQL_MARKER
private static final Marker QUERY_MARKER = MarkerManager.getMarker("SQL_QUERY", SQL_MARKER);
public String doQuery(String table) {
LOGGER.entry(table);
LOGGER.debug(QUERY_MARKER, "SELECT * FROM {}", table); // select.log 파일에 출력됨
return LOGGER.exit();
}
public String doUpdate(String table, Map<String, String> params) {
LOGGER.entry(table);
LOGGER.debug(UPDATE_MARKER, "UPDATE {} SET {}", table, formatCols()); // update.log 파일에 출력됨
return logger.exit();
}
...
<Appenders>
<File name="fileQuery" fileName="./logs/file/select.log">
<MarkerFilter marker="SQL_QUERY" onMatch="ACCEPT" onMismatch="DENY" />
<PatternLayout pattern="%level %m%n" />
</File>
<File name="fileUpdate" fileName="./logs/file/update.log">
<MarkerFilter marker="SQL_UPDATE" onMatch="ACCEPT" onMismatch="DENY" />
<PatternLayout pattern="%level %m%n" />
</File>
...
</Appenders>
...
<!-- Log4j 1.2 -->
<!--
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2</version>
</dependency>
-->
<!-- Log4j 2 -->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
<version>x.x.x</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>x.x.x</version>
</dependency>
기존 Log4j 1.x API가 Log4j 2 API로 변환 처리될 수 있도록 Log4j 2 Bridge를 추가한다.
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-1.2-api</artifactId>
<version>x.x.x</version>
</dependency>
Log4j 2의 Logger 객체를 생성할 수 있도록, Logger 생성 메서드를 다음과 같이 변경한다.
| Log4j 1.x | Log4j 2 | |
|---|---|---|
| Package | org.apache.log4j | org.apache.logging.log4j |
| Logger 생성 | org.apache.log4j.Logger logger = org.apache.log4j.Logger.getLogger(); | org.apache.logging.log4j.Logger logger = org.apache.logging.log4j.LogManager.getLogger(); |
Log4j 2에서는 설정 태그들이 직관적이고 간단하게 변경되었다. 더 자세한 설명은 Log4j 2 상세 설정 을 참조하도록 한다.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE log4j:configuration PUBLIC "-//APACHE//DTD LOG4J 1.2//EN" "log4j.dtd">
<log4j:configuration xmlns:log4j='http://jakarta.apache.org/log4j/'>
<appender name="STDOUT" class="org.apache.log4j.ConsoleAppender">
<layout class="org.apache.log4j.PatternLayout">
<param name="ConversionPattern" value="%d %-5p [%t] %C{2} (%F:%L) - %m%n"/>
</layout>
</appender>
<category name="org.apache.log4j.xml">
<priority value="info" />
</category>
<Root>
<priority value ="debug" />
<appender-ref ref="STDOUT" />
</Root>
</log4j:configuration>
<?xml version="1.0" encoding="UTF-8"?>
<Configuration>
<Appenders>
<Console name="STDOUT" target="SYSTEM_OUT">
<PatternLayout pattern="%d %-5p [%t] %C{2} (%F:%L) - %m%n"/>
</Console>
</Appenders>
<Loggers>
<Logger name="org.apache.log4j.xml" level="info"/>
<Root level="debug">
<AppenderRef ref="STDOUT"/>
</Root>
</Loggers>
</Configuration>
Migration to Log4j 2
Log4j 2 API Documentation
Log4j 2 Implementation Documentation
Log4j 2 환경 설정(Appender, Layout, Log Level 등)을 코드 내에서 직접 제어할 수 있다.
아래는 별도의 외부 설정파일 없이도 로깅할 수 있는 방법을 설명한다.
별도의 Log4j 2 설정파일 없이도 코드 내에서 Logger 객체를 획득하여 로깅이 가능하다.
LogManager.getLogger() 메서드를 통해 Logger 객체를 생성하며, Log4j 2는 디폴트로 설정된 Logger 객체를 반환한다.
디폴트 Logger 객체의 기본적인 디폴트 설정은 다음과 같다.
Log Level : ERROR
Appender : ConsoleAppender
Layout : PatternLayout
pattern : %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
위 Logger 객체를 org.apache.logging.log4j.core.Logger로 캐스팅하면, Log Level, Appender, Layout 등 설정을 변경할 수 있다.
예시에서 사용된 설정 관련 메서드는 다음과 같다. 더 자세한 정보는 Log4j 2 API를 참조하도록 한다.
LogManager.getLogger() -- Logger 생성
Logger.addAppender(), Logger.removeAppender(), Logger.getAppenders() -- Logger에 Appender 추가 및 제거, 획득
Layout.createLayout() -- Layout 생성
Appender.createAppender(), Appender.removeAppender() -- Appender 생성 및 삭제
Logger.setLevel(), Logger.getLevel() -- Log Level 설정 및 획득
@Test
public void log4j2ConfigTest() {
// 디폴트 Logger 생성
Logger logger = (Logger) LogManager.getLogger();
// 디폴트 설정 확인
// Log Level: ERROR
assertEquals(Level.ERROR, logger.getLevel());
// Appender: Console
Map<String, Appender> appenders = logger.getAppenders();
assertEquals(1, appenders.size());
assertEquals(ConsoleAppender.class, appenders.get("Console").getClass());
// Layout: Pattern
ConsoleAppender console = (ConsoleAppender) appenders.get("Console");
assertEquals(PatternLayout.class, console.getLayout().getClass());
PatternLayout pattern = (PatternLayout) console.getLayout();
assertEquals("%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n", pattern.toString());
// 설정 변경 전: ERROR Level 이상 로그만 출력됨
logger.debug("변경 전: [DEBUG] Test log4j 2.");
logger.info("변경 전: [INFO] Test log4j 2.");
logger.warn("변경 전: [WARN] Test log4j 2.");
logger.error("변경 전: [ERROR] Test log4j 2.");
logger.fatal("변경 전: [FATAL] Test log4j 2.");
/*
출력결과
===================
14:17:09.930 [main] ERROR egovframework.rte.fdl.logging.LogTest - 변경 전: [ERROR] Test log4j 2.
14:17:09.931 [main] FATAL egovframework.rte.fdl.logging.LogTest - 변경 전: [FATAL] Test log4j 2
*/
// 디폴트 설정 변경
logger.removeAppender(console);
// Appender: File
PatternLayout layout = PatternLayout.createLayout("%m%n", null, null, null, null, null);
FileAppender file = FileAppender.createAppender("./logs/file/promatic.log", "false", "false", "file", "false", "true", "false", null, layout, null, "false", null, null);
logger.addAppender(file);
// Log Level: DEBUG
logger.setLevel(Level.DEBUG);
// 설정 변경 후: DEBUG Level 이상 로그가 file(promatic.log)에 출력됨
logger.debug("변경 후: [DEBUG] Test log4j 2.");
logger.info("변경 후: [INFO] Test log4j 2.");
logger.warn("변경 후: [WARN] Test log4j 2.");
logger.error("변경 후: [ERROR] Test log4j 2.");
logger.fatal("변경 후: [FATAL] Test log4j 2.");
/*
출력결과
===================
변경 후: [DEBUG] Test log4j 2.
변경 후: [INFO] Test log4j 2.
변경 후: [WARN] Test log4j 2.
변경 후: [ERROR] Test log4j 2.
변경 후: [FATAL] Test log4j 2.
*/
}
Log4j 2는 기존 Properties 파일 형식의 환경 설정을 지원하지 않으며,
XML (log4j2.xml) 혹은 JSON (log4j2.json or log4j2.jsn) 파일 형식의 환경 설정만 가능하다.
아래는 XML 파일을 이용한 환경 설정에 대해서만 다루며, JOSN 방식은 Log4j 2 매뉴얼을 참고하도록 한다.
XML 파일(log4j2.xml)을 작성하고, WEB-INF/classes 하위에 포함될 수 있도록 위치시킨다.
Log4j 2가 초기화될 때 자동으로 위 설정 파일을 읽어들인다.
Log4j 2에서는 XML 파일의 최상위 요소가 <Configuration> 으로 변경되었다.
<Configuration> 요소 아래에 Logger, Appender, Layout 설정 등과 관련한 하위 요소를 정의한다.
<?xml version="1.0" encoding="UTF-8"?>
<Configuration>
<!-- Appender, Layout 설정 -->
<Appenders>
<Console name="console" target="SYSTEM_OUT">
<PatternLayout/>
</Console>
<File name="file" fileName="./logs/file/sample.log" append="false">
<PatternLayout pattern="%d %5p [%c] %m%n"/>
</File>
</Appenders>
<!-- Logger 설정 -->
<Loggers>
<Logger name="egovLogger" level="DEBUG" additivity="false">
<AppenderRef ref="console"/>
<AppenderRef ref="file"/>
</Logger>
<Rootlevel="ERROR">
<AppenderRef ref="console"/>
</Root>
</Loggers>
</Configuration>
Log4j 2 Configuration
XML Syntax
JSON Syntax
Logger는 로깅 작업을 수행하는 Log4j 주체로, Logger 설정을 제외한 모든 로깅 기능이 이 Logger를 통해 처리된다.
사용자는 어플리케이션 내에서 사용할 Logger를 정의해야 하며, Log Level과 Appender 설정에 따라 출력 대상과 위치가 결정된다.
Root Logger를 포함한 모든 Logger는 상위 요소인 <Loggers> 아래에 선언한다.
Root Logger는 <Root> 요소로, 일반 Logger는 <Logger> 요소로 정의한다.
Logger는 하나 이상 정의할 수 있으며, Root 요소를 반드시 정의해야 한다.
<Loggers>
<Logger>...</Logger>
<Root>...</Root>
</Loggers>
<Loggers>
<!-- attribute: name(Logger명), level(Log Level), additivity(중복로깅여부, true or false) -->
<!-- element: AppenderRef(Appender명) -->
<Logger name="X.Y" level="INFO" additivity="false">
<AppenderRef ref="console"/>
</Logger>
<Logger name="X" level="DEBUG" additivity="false">
<AppenderRef ref="console"/>
</Logger>
<Rootlevel="ERROR">
<AppenderRef ref="console"/>
</Root>
</Loggers>
위에서 AppenderRef 요소에 지정한 “console” Appender가 없는 경우, 정상적인 로깅이 수행될 수 없다.
Logger는 코드 내에서 다음과 같은 방법으로 호출할 수 있다.
package egovframe.sample;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
public class LoggerTest {
// (1) Logger Name이 "egovframe.sample.LoggerTest"인 Logger 설정을 따르는 Logger 객체 생성
Logger logger1 = LogManager.getLogger();
// (2) 위와 동일
Logger logger2 = LogManager.getLogger(LoggerTest.class);
// (3) Logger Name이 "X"인 Logger설정을 따르는 Logger 객체 생성
Logger logger3 = LogManager.getLogger("X");
}
(1), (2)과 같이 Logger Name에 해당하는 Logger가 설정 파일에 없는 경우, 다음 Logger Hierarchy 규칙에 따라 결정된다.
결과적으로 (1), (2)에서 생성된 Logger 객체는 Root Logger 설정을 따른다.
사용자가 호출한 Logger 객체가 어떤 설정을 따르는지 이해하기 위해서는 Logger Hierarchy를 알고 있어야 한다.
내부적으로 설정 파일에 정의된 각 Logger 설정에 따라 LoggerConfig 오브젝트가 생성되며,
Logger Name에 따라 오브젝트 간 부모-자식 관계가 성립한다. 즉 부모 Logger의 설정을 자식 Logger가 상속받는다.
예를 들어 “X.Y” Logger의 부모는 “X"이고, “X” Logger의 부모는 Root Logger(최상위)이다.
다음은 Hierarchy 규칙과 예시이다.
| Logger Name | Assigned LoggerConfig | Level | Java Code | Description |
|---|---|---|---|---|
| root | root | ERROR | LogManager.getLogger(“root”); | 설정 파일의 Root 설정을 따름 |
| X | X | DEBUG | LogManager.getLogger(“X”); | 설정 파일의 X Logger 설정을 따름 |
| X.Y | X.Y | INFO | LogManager.getLogger(“X.Y”); | 설정 파일의 X.Y Logger 설정을 따름 |
| X.Y.Z | X.Y | INFO | LogManager.getLogger(“X.Y.Z”); | X.Y.Z Logger 설정이 없으므로, 부모인 X.Y 설정을 따름 |
| X.YZ | X | DEBUG | LogManager.getLogger(“X.YZ”); | X.YZ Logger 설정이 없으므로, 부모인 X 설정을 따름 |
| Y | root | ERROR | LogManager.getLogger(“Y”); | Y Logger 설정이 없으므로, 부모인 Root 설정을 따름 |
Log4j 2는 FATAL, ERROR, WARN, INFO, DEBUG, TRACE의 Log Level을 제공한다.
각각 trace(), debug(), info(), warn(), error(), fatal()라는 로깅 메서드를 이용해 로그를 출력할 수 있다.
로그 레벨은 다음과 같다. (FATAL > ERROR > WARN > INFO > DEBUG > TRACE)
| 로그 레벨 | 설명 |
|---|---|
| FATAL | 아주 심각한 에러가 발생한 상태를 나타냄. 시스템적으로 심각한 문제가 발생해서 어플리케이션 작동이 불가능할 경우가 해당하는데, 일반적으로는 어플리케이션에서는 사용할 일이 없음. |
| ERROR | 요청을 처리하는중 문제가 발생한 상태를 나타냄. |
| WARN | 처리 가능한 문제이지만, 향후 시스템 에러의 원인이 될 수 있는 경고성 메시지를 나타냄. |
| INFO | 로그인, 상태변경과 같은 정보성 메시지를 나타냄. |
| DEBUG | 개발 시 디버그 용도로 사용한 메시지를 나타냄. |
| TRACE | 디버그 레벨이 너무 광범위한 것을 해결하기 위해서 좀더 상세한 상태를 나타냄. |
어플리케이션 수행 중 Log Level을 변경할 수도 있다. 이 때 Logger Configuration을 변경하는 것이므로, Logger 설정 정보를 참조하는 메서드를 호출할 수 있도록 org.apache.logging.log4j.Logger를 org.apache.logging.log4j.core.Logger로 캐스팅해야 한다.
Log Level 변경하려면 변경할 Level값을 파라미터로 setLevel() 메서드를 호출한다.
setLevel() 호출 이후부터 Log Level이 변경되며, 지정된 로그레벨 이하의 Log Event는 무시된다.
package egovframe.sample;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
public class LoggerTest {
Logger logger = LogManager.getLogger(); // Root Logger 설정을 따름, Log Level: ERROR
org.apache.logging.log4j.core.Logger targetLogger = (org.apache.logging.log4j.core.Logger) logger;
targetLogger.debug("변경 전 - debug"); // 출력됨
targetLogger.error("변경 전 - error"); // 출력 안됨
targetLogger.setLevel(Level.DEBUG); // DEBUG, INFO, WARN, ERROR, FATAL 출력 가능
targetLogger.debug("변경 후 - debug"); // 출력됨
targetLogger.error("변경 후 - error"); // 출력됨
}
자바에서는 C와 같이 전처리기의 기능이 없기 때문에 #ifdef DEBUG와 같은 형태와 같이 디버깅 때와 릴리즈 때의 디버깅코드를 각각 별도로 생성할 수가 없다. 따라서 Log4j의 이러한 기능은 로그관리에 있어서 상당히 편리하다.
자세한 설정은 Log4j 2 Logger 매뉴얼을 참고하도록 한다.
Appender는 로그가 출력되는 위치를 나타낸다.
XXXAppender로 끝나는 클래스들의 이름을 보면, 출력 위치를 어느 정도 짐작할 수 있다.
Log4j 2는 Console, File, RollingFile, Socket, DB 등 다양한 로그 출력 위치와 방법을 지원한다.
기존 Log4j 1.x와 크게 달라진 점은 Appender 종류를 class 속성값으로 구분한 것과 달리, Log4j 2에서는 태그로 구분한다.
본 페이지에서는 자주 사용되는 Console, File, RollingFile, JDBC Appender에 대해서만 설명한다.
출력 위치에 따라 Appender 종류와 설정 태그가 달라지며, 아래 표는 각 Appender 정의 태그와 출력 위치이다.
| Appenders | 태그명 | 출력 위치 |
|---|---|---|
| ConsoleAppender | <Console> | 콘솔에 출력 |
| FileAppender | <File> | 파일에 출력 |
| RollingFileAppender | <RollingFile> | 조건에 따라 파일에 출력 |
| JDBCAppender | <JDBC> | RDB Table에 출력 |
모든 Appender 요소는 상위 요소인 <Appenders> 아래에 선언한다.
<Appenders>
<Console>...</Console>
<File>...</File>
<RollingFile>...</RollingFile>
<JDBC>...</JDBC>
</Appenders>
Appender 요소는 name 속성값을 가지며, name 속성에 Appender 이름을 지정한다. name 속성값은 Logger가 로그 출력에 사용할 Appender를 참조하기 위해 사용한다. 또한 Appender 요소의 하위 요소로 로그 출력 패턴인 Layout을 정의한다.
아래는 ConsoleAppender와 PatternLayout을 사용한 샘플코드이다.
<Appenders>
<Console name="console" target="SYSTEM_OUT">
<PatternLayout /> <!-- 디폴트 패턴 적용, %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n -->
</Console>
</Appenders>
<Loggers>
<Logger name="egovLogger" level="DEBUG" additivity="false">
<AppenderRef ref="console" />
</Logger>
<Rootlevel="ERROR">
<AppenderRef ref="console" />
</Root>
</Loggers>
다음은 각 Appender 정의 시 필요한 기본 설정에 대한 설명이다.
로그를 콘솔에 출력하기 위한 Appender
<!-- attribute: name(Appender명), target(출력방향지정, "SYSTEM_OUT" or "SYSTEM_ERR"(default)), follow, ignoreExceptions -->
<!-- element: Layout(출력패턴설정), Filters -->
<Console name="console" target="SYSTEM_OUT">
<PatternLayout pattern="%d %5p [%c] %m%n" />
</Console>
로그를 파일에 출력하기 위한 Appender
<!-- attribute: name(Appender명), fileName(target파일명), append(이어쓰기여부, true(default) or false), locking, immediateFlush, ignoreExceptions, bufferedIO -->
<!-- element: Layout(출력패턴설정), Filters -->
<!-- append="false"이면 매번 로깅 시 기존 로그 파일을 clear하고 새로 로깅 -->
<File name="file" fileName="./logs/file/sample.log" append="false">
<PatternLayout pattern="%d %5p [%c] %m%n" />
</File>
<File name="mdcFile" fileName="./logs/file/mdcSample.log" append="false">
<!-- Thread Context Map(also known as MDC) 객체의 key와 매칭되는 value를 로깅 - %X{key} -->
<!-- ex) ThreadContext.put(“testKey”, “testValue”);인 경우, 레이아웃 패턴 %X{testKey}에 의해 “testValue” 로깅 -->
<PatternLayout pattern="%d %5p [%c] [%X{class} %X{method} %X{testKey}] %m%n" />
</File>
TriggeringPolicy와 RolloverStrategy에 따라 로그를 파일에 출력하기 위한 Appender로,
FileAppender는 지정한 파일에 로그가 계속 남으므로 한 파일의 크기가 지나치게 커질 수 있고, 계획적인 로그관리가 불가능해진다.
그러나 RollingFileAppender는 파일의 크기 또는 파일 백업 인덱스 등의 지정을 통해서 특정 크기 이상으로 파일 크기가 커지게 되면,
기존파일(target)을 백업파일(history)로 바꾸고, 다시 처음부터 로깅을 시작한다.
<!-- attribute: name(Appender명), fileName(target파일명), filePattern(history파일명), append, immediateFlush, ignoreExceptions, bufferedIO -->
<!-- element: Layout(출력패턴설정), Filters, Policy(file rolling 조건 설정), Strategy(file name과 location 관련 설정) -->
<RollingFile name="rollingFile" fileName="./logs/rolling/rollingSample.log" filePattern="./logs/rolling/rollingSample.%i.log">
<PatternLayout pattern="%d %5p [%c] %m%n" />
<Policies>
<!-- size 단위: Byte(default), KB, MB, or GB -->
<SizeBasedTriggeringPolicy size="1000" />
</Policies>
<!-- 기존 maxIndex 속성이 Strategy 엘리먼트로 변경됨 -->
<!-- index는 min(default 1)부터 max(default 7)까지 증가, 아래에는 max="3"으로 settting -->
<!-- fileIndex="min"이므로 target file의 size가 1000 byte를 넘어가면, fileIndex가 1(min)인 history file에 백업 (fixed window strategy) -->
<!-- 그 다음 1000 byte를 넘어가면, rollingSample.1.log을 rollingSample.2.log 파일에 복사하고, target 파일을 rollingSample.1.log에복사한 후 target 파일에 새로 로깅 -->
<DefaultRolloverStrategy max="3" fileIndex="min" />
</RollingFile>
<RollingFile name="rollingFile" fileName="./logs/rolling/dailyRollingSample.log" filePattern="./logs/daily/dailyRollingSample.log.%d{yyyy-MM-dd-HH-mm-ss}">
<PatternLayout pattern="%d %5p [%c] %m%n" />
<Policies>
<!-- interval(default 1)이므로 1초 간격으로 rolling 수행 -->
<TimeBasedTriggeringPolicy />
</Policies>
</RollingFile>
로그를 RDB에 출력하기 위한 Appender로,
Connection 객체를 제공하기 위한 JNDI DataSource 혹은 Connection Factory Method를 함께 정의해야한다.
<!-- attribute: name(Appender명), tableName(RDB Table명), columnConfigs, filter, bufferSize, ignoreExceptions, connectionSource -->
<!-- element: DataSource(jndi datasource 정보), ConnectionFactory(Connection Factory 정보), Column(Table Column명) -->
<!-- 테이블명이 db_log인 테이블에 로깅됨 -->
<JDBC name="db" tableName="db_log">
<!-- DB Connection을 제공해줄 클래스와 메서드명 정의 -->
<!-- JDBCAppender가 EgovConnectionFactory.getDatabaseConnection() 메서드를 호출 -->
<ConnectionFactory class="egovframework.rte.fdl.logging.db.EgovConnectionFactory" method="getDatabaseConnection" />
<!-- log event가 insert될 컬럼 설정, insert될 값은 PatternLayout의 pattern 사용 -->
<Column name="eventDate" isEventTimestamp="true" />
<Column name="level" pattern="%p" />
<Column name="logger" pattern="%c" />
<Column name="message" pattern="%m" />
<Column name="exception" pattern="%ex{full}" />
</JDBC>
표준프레임워크에서는 Connection Factory 역할을 하는 EgovConnectionFactory를 제공하고 있으며, 어플리케이션에서 설정한 dataSource 빈을 주입받아 싱글톤을 생성한다. 이를 위해 다음과 같이 빈정의를 추가해야한다.
<bean id="egovConnectionFactory" class="egovframework.rte.fdl.logging.db.EgovConnectionFactory">
<property name="dataSource" ref="dataSource" />
</bean>
WAS에서 제공하는 DataSource를 사용하려면, 위 <ConnectionFactory> 부분을 <DataSource jndiName=”…” />으로 변경한다.
각 Appender 요소에서 정의할 수 있는 하위 요소와 속성이 다르므로, 자세한 설정은 각 매뉴얼을 참조하도록 한다.
Log4j 2 Appneders
ConsoleAppender
FileAppender
RollingFileAppender
JDBCAppender
Layout은 발생한 로그 이벤트의 포맷을 지정하고, 원하는 형식으로 로그를 출력할 수 있다.
Appenders 설정과 마찬가지로 Log4j 2에서는 Layout을 class 속성이 아닌 태그로 구분한다.
출력 형식에 따라 Layout의 종류가 달라지며, 아래와 같은 Layouts을 제공한다.
| 내용 | |||||
| HTMLLayout | PatternLayout | RFC5424Layout | SerializedLayout | SyslogLayout | XMLLayout |
본페이지는 위의 Layouts 중 일반적으로 디버깅에 가장 적합한 PatternLayout만 설명한다.
PatternLayout은 Appender 요소의 하위 요소로 정의한다.
<Console>
<!-- 디폴트 패턴 적용, "%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n" -->
<PatternLayout/>
</Console>
<PatternLayout/>을 선언하면 디폴트 pattern이 적용되며, pattern 속성을 이용하여 일자, 시간, 클래스, 로거명, 메시지 등 여러 정보를 선택하여 다양한 조합의 로그 메시지를 출력할 수 있다.
%로 시작하고 %뒤에는 format modifiers와 conversion character로 정의한다.
예) %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
| 패턴 | 설명 |
|---|---|
| c, logger | 로깅 이벤트를 발생시키기 위해 선택한 로거의 이름을 출력 |
| C, class | 로깅 이벤트가 발생한 클래스의 풀네임명을 출력 |
| M, method | 로깅 이벤트가 발생한 메서드명을 출력 |
| F, file | 로깅 이벤트가 발생한 클래스의 파일명을 출력 |
| l, location | 로깅 이벤트가 발생한 클래스의 풀네임명.메서드명(파일명:라인번호)를 출력 |
| d, date | 로깅 이벤트의 일자와 시간을 출력,\\SimpleDateFormat클래스에 정의된 패턴으로 출력 포맷 지정가능 |
| L, line | 로깅 이벤트가 발생한 라인 번호를 출력 |
| m, msg, message | 로그문에서 전달된 메시지를 출력 |
| n | 줄바꿈 |
| p, level | 로깅 이벤트의 레벨을 출력 |
| r, relative | 로그 처리시간 (milliseconds) |
| t, thread | 로깅 이벤트가 발생한 스레드명을 출력 |
| %% | %를 출력하기 위해 사용하는 패턴 |
시스템을 개발할 때 필요한 유일한 ID를 생성하기 위해 사용하도록 서비스한다.
UUID는 OSF(Open Software Foundation)에 의해 제정된 고유식별자(Identifier)에 대한 표준이다. UUID는 16-byte (128-bit)의 숫자로 구성된다.
UUID를 표현하는 방식에 대한 특별한 규정은 없으나, 일반적으로 아래와 같이 16진법으로 8-4-4-4-12 형식으로 표현한다.
550e8400-e29b-41d4-a716-446655440000
UUID 표준은 아래 문서에 기술되어 있다.
UUID는 다음 5개의 Version이 존재한다.
ID 생성 방식으로는 UUID를 생성하는 UUID Generation Service와 sequence를 활용하는 Sequence Id Generation Service, 그리고 키제공을 위한 테이블을 지정하여 생성하는 Table Id Generation Service 3가지가 있다.
새로운 ID를 생성하기 위해 UUID 생성 알고리즘을 이용하여 16 바이트 길이의 ID를 생성한다.
String 타입의 ID 생성과 BigDecimal 타입의 ID 생성 두가지 유형 ID 생성을 지원한다.
지원하는 방법은 설정에 따라서 Mac Address Base Service, IP Address Base Service, No Address Base Service 세가지 유형이 있다.
MAC Address를 기반으로 유일한 Id를 생성하는 UUIdGenerationService
<bean name="UUIdGenerationService" class="org.egovframe.rte.fdl.idgnr.impl.EgovUUIdGnrServiceImpl">
<property name="address">
<value>00:00:F0:79:19:5B</value>
</property>
</bean>
@Resource(name="UUIdGenerationService")
private EgovIdGnrService uUidGenerationService;
@Test
public void testUUIdGeneration() throws Exception {
assertNotNull(uUidGenerationService.getNextStringId());
assertNotNull(uUidGenerationService.getNextBigDecimalId());
}
IP Address를 기반으로 유일한 Id를 생성하는 UUIdGenerationService
<bean name="UUIdGenerationServiceWithIP" class="org.egovframe.rte.fdl.idgnr.impl.EgovUUIdGnrServiceImpl">
<property name="address">
<value>100.128.120.107</value>
</property>
</bean>
@Resource(name="UUIdGenerationServiceWithIP")
private EgovIdGnrService uUIdGenerationServiceWithIP;
@Test
public void testUUIdGenerationIP() throws Exception {
assertNotNull(uUIdGenerationServiceWithIP.getNextStringId());
assertNotNull(uUIdGenerationServiceWithIP.getNextBigDecimalId());
}
IP Address 설정없이 Math.random()을 이용하여 주소정보를 생성하고 유일한 Id를 생성하는 UUIdGenerationService
<bean name="UUIdGenerationServiceWithoutAddress" class="org.egovframe.rte.fdl.idgnr.impl.EgovUUIdGnrServiceImpl">
</bean>
@Resource(name="UUIdGenerationServiceWithoutAddress")
private EgovIdGnrService uUIdGenerationServiceWithoutAddress;
@Test
public void testUUIdGenerationNoAddress() throws Exception {
assertNotNull(uUIdGenerationServiceWithoutAddress.getNextStringId());
assertNotNull(uUIdGenerationServiceWithoutAddress.getNextBigDecimalId());
}
새로운 ID를 생성하기 위해 Database의 SEQUENCE를 사용하는 서비스이다. 서비스를 이용하는 시스템에서 Query를 지정하여 아이디를 생성할 수 있도록 하고 Basic Type Service와 BigDecimal Type Service 두가지를 지원한다.
기본타입 ID를 제공하는 서비스로 int, short, byte, long 유형의 ID를 제공한다.
CREATE SEQUENCE idstest MINVALUE 0;
<bean name="primaryTypeSequenceIds" class="org.egovframe.rte.fdl.idgnr.impl.EgovSequenceIdGnrServiceImpl" destroy-method="destroy">
<property name="dataSource" ref="dataSource"/>
<property name="query" value="SELECT idstest.NEXTVAL FROM DUAL"/>
</bean>
@Resource(name="primaryTypeSequenceIds")
private EgovIdGnrService primaryTypeSequenceIds;
@Test
public void testPrimaryTypeIdGeneration() throws Exception {
//int
assertNotNull(primaryTypeSequenceIds.getNextIntegerId());
//short
assertNotNull(primaryTypeSequenceIds.getNextShortId());
//byte
assertNotNull(primaryTypeSequenceIds.getNextByteId());
//long
assertNotNull(primaryTypeSequenceIds.getNextLongId());
}
BigDecimal ID를 제공하는 서비스로 기본타입 ID 제공 서비스 설정에 추가적으로 useBigDecimals을 true로 설정하여 BigDecimal 사용하도록 한다.
CREATE SEQUENCE idstest MINVALUE 0;
<bean name="bigDecimalTypeSequenceIds" class="org.egovframe.rte.fdl.idgnr.impl.EgovSequenceIdGnrServiceImpl" destroy-method="destroy">
<property name="dataSource" ref="dataSource"/>
<property name="query" value="SELECT idstest.NEXTVAL FROM DUAL"/>
<property name="useBigDecimals" value="true"/>
</bean>
@Resource(name="bigDecimalTypeSequenceIds")
private EgovIdGnrService bigDecimalTypeSequenceIds;
@Test
public void testBigDecimalTypeIdGeneration() throws Exception {
//BigDecimal
assertNotNull(bigDecimalTypeSequenceIds.getNextBigDecimalId());
}
CREATE SEQUENCE <sequence name> [START WITH <start value>] [INCREMENT BY <increment value>] [MINVALUE <min value>] [MAXVALUE <max value>]
SELECT <sequence name>.NEXTVAL FROM DUAL
CREATE SEQUENCE <sequence name> [AS {INTEGER | BIGINT}] [START WITH <start value>] [INCREMENT BY <increment value>]
SELECT NEXT VALUE FOR <sequence name> FROM DUAL
-- HSQL DB는 DUAL 테이블을 제공하지 않기 때문에 하나의 row를 가진 DUAL 테이블을 수동으로 생성해야 한다.
CREATE SEQUENCE <sequence name> [START WITH <start value>] [INCREMENT BY <increment value>] [MINVALUE <min value>] [MAXVALUE <max value>]
SELECT NEXT VALUE FOR <sequence name> FROM SYSIBM.SYSDUMMY1
새로운 아이디를 얻기 위해 별도의 테이블을 생성, 키값과 키값에 해당하는 아이디값을 입력하여 관리를 제공하는 서비스이다.
table_name(CHAR 또는 VARCHAR타입), next_id(integer 또는 DECIMAL type)와 같이 두 칼럼을 필요로 한다.
별도의 테이블에 설정된 정보만을 사용하여 제공하는 Basic Service, prefix와 채울 문자열을 지정하여 String ID를 생성할 수 있는 Strategy Base Service를 제공한다.
테이블에 지정된 정보에 의해서 아이디를 생성하는 서비스로 사용하고자 하는 시스템에서 테이블을 생성해서 사용할 수 있다.
CREATE TABLE ids (
table_name varchar(16) NOT NULL,
next_id DECIMAL(30) NOT NULL,
PRIMARY KEY (table_name)
);
INSERT INTO ids VALUES('id','0');
-- ID Generation 서비스를 쓰고자 하는 시스템에서 미리 생성해야 할 DB Schema 정보이다.
<bean name="basicService" class="org.egovframe.rte.fdl.idgnr.impl.EgovTableIdGnrServiceImpl" destroy-method="destroy">
<property name="dataSource" ref="dataSource"/>
<property name="blockSize" value="10"/>
<property name="table" value="ids"/>
<property name="tableName" value="id"/>
</bean>
tableName이라고 지정함@Resource(name="basicService")
private EgovIdGnrService basicService;
@Test
public void testBasicService() throws Exception {
//int
assertNotNull(basicService.getNextIntegerId());
//short
assertNotNull(basicService.getNextShortId());
//byte
assertNotNull(basicService.getNextByteId());
//long
assertNotNull(basicService.getNextLongId());
//BigDecimal
assertNotNull(basicService.getNextBigDecimalId());
//String
assertNotNull(basicService.getNextStringId());
}
아이디 생성을 위한 룰을 등록하고 룰에 맞는 아이디를 생성할 수 있도록 지원하는 서비스이다.
위의 Basic Service에서 추가적으로 Strategy 정보 설정을 추가하여 사용 할 수 있다.
단, 이 서비스는 String 타입의 ID만을 제공한다.
CREATE TABLE idttest(
table_name varchar(16) NOT NULL,
next_id DECIMAL(30) NOT NULL,
PRIMARY KEY (table_name)
);
INSERT INTO idttest VALUES('test','0');
-- ID Generation 서비스를 쓰고자 하는 시스템에서 미리 생성해야 할 DB Schema 정보이다.
<bean name="Ids-TestWithGenerationStrategy" class="org.egovframe.rte.fdl.idgnr.impl.EgovTableIdGnrServiceImpl" destroy-method="destroy">
<property name="dataSource" ref="dataSource"/>
<property name="strategy" ref="strategy"/>
<property name="blockSize" value="1"/>
<property name="table" value="idttest"/>
<property name="tableName" value="test"/>
</bean>
<bean name="strategy" class="org.egovframe.rte.fdl.idgnr.impl.strategy.EgovIdGnrStrategyImpl">
<property name="prefix" value="TEST-"/>
<property name="cipers" value="5"/>
<property name="fillChar" value="*"/>
</bean>
@Resource(name="Ids-TestWithGenerationStrategy")
private EgovIdGnrService idsTestWithGenerationStrategy;
@Test
public void testIdGenStrategy() throws Exception {
initializeNextLongId("test", 1);
// prefix : TEST-, cipers : 5, fillChar :*)
for (int i = 0; i < 5; i++)
assertEquals("TEST-****" + (i + 1), idsTestWithGenerationStrategy.getNextStringId());
}
Property는 시스템의 설치 환경에 관련된 정보나, 잦은 정보의 변경이 요구되는 경우 외부에서 그 정보를 관리하게 함으로써 시스템의 유연성을 높이기 위해서 제공하는 것으로 Property Service와 Property Source를 제공하고 있다. Property Service와 Property Source는 각각의 특성과 용도에 따라 시스템의 설정 정보를 관리한다. 이와 같은 기능을 통해 전자정부프레임워크는 시스템의 유연성과 확장성을 높여준다.
Property Service 는 시스템의 설치 환경에 관련된 정보나, 잦은 정보의 변경이 요구되는 경우 외부에서 그 정보를 관리하게 함으로써 시스템의 유연성을 높이기 위해서 제공하는 것으로 Spring Bean 설정 파일에 관리하고자 하는 정보를 입력(Bean 설정 파일 사용) 하거나 외부 파일에 정보 입력 후에 Bean 설정 파일에서 그 파일 위치를 입력하여 이용(외부 설정 파일 사용)할 수 있다.
간단하게 설정하고자 할 때 사용할 수 있는 방법으로 별도의 외부파일을 두지 않고 Spring Bean 설정 파일을 이용할 수 있다. 하지만 어플리케이션 운영 중에 Property 정보 변경은 불가능 하고 변경처리 시 어플리케이션을 재기동해야 한다. 사용하기 위해서는 bean property의 Name에 properties라고 입력하고 map entry의 key에 관리하고자 하는 키, value에 관리하고자 하는 값을 입력하여 설정한다.
<bean name="propertyService" class="egovframework.rte.fdl.property.impl.EgovPropertyServiceImpl"
destroy-method="destroy">
<property name="properties">
<map>
<entry key="AAAA" value="1234"/>
</map>
</property>
</bean>
<bean id="messageSource"
class="org.springframework.context.support.ReloadableResourceBundleMessageSource">
<property name="basenames">
<list>
<value>classpath:/message/message-common</value>
<value>classpath:/egovframework/rte/fdl/idgnr/messages/idgnr</value>
<value>classpath:/egovframework/rte/fdl/property/messages/properties</value>
</list>
</property>
<property name="cacheSeconds">
<value>60</value>
</property>
</bean>
@Resource(name="propertyService")
protected EgovPropertyService propertyService ;
@Test
public void testPropertiesService() throws Exception {
assertEquals("1234",propertyService.getString("AAAA"));
}
| 제공유형 | 설정 방법 | 사용 방법 |
|---|---|---|
| String | key=“A” value=“ABC” | propertyService.getString(“A”) |
| boolean | key=“B” value=“true” | propertyService.getBoolean(“B”) |
| int | key=“C” value=“123” | propertyService.getInt(“C”) |
| long | key=“D” value=“123” | propertyService.getLong(“D”) |
| short | key=“E” value=“123” | propertyService.getShort(“E”) |
| float | key=“F” value=“123” | propertyService.getFloat(“F”) |
| Vector | key=“G” value=“123,456” | propertyService.getVector(“G”) |
별도의 Property 파일을 만들어서 사용하는 방법으로 Spring Bean 설정 파일에는 파일의 위치를 입력하여 이용할 수 있다. 외부 설정 파일에 기재된 프로퍼티 내용은 어플리케이션 운영 중에 추가 및 변경 가능하다.
<bean name="propertyService" class="egovframework.rte.fdl.property.impl.EgovPropertyServiceImpl"
destroy-method="destroy">
<property name="extFileName">
<set>
<map>
<entry key="encoding" value="UTF-8"/>
<entry key="filename" value="file:./src/**/refresh-resource.properties"/>
</map>
<value>classpath*:properties/resource.properties</value>
</set>
</property>
</bean>
AAAA=1234
@Resource(name="propertyService")
protected EgovPropertyService propertyService ;
@Test
public void testPropertiesService() throws Exception {
assertEquals("1234",propertyService.getString("AAAA"));
}
${}를 사용해 외부 설정 값을 참조하며, Spring 3.1 이후에는 PropertySourcesPlaceholderConfigurer가 사용된다. DB PropertySource는 DB 테이블에서 설정 값을 가져오는 기능을 제공하며, DBPropertySourceInitializer를 통해 WAS 기동 시 설정 값을 로드할 수 있다.Property Source는 property place-holder를 이용하여 xml의 bean설정에서 key값을 통해 가져올 수 있으며 코드상에서는 Environment를 이용하여 해당값을 가져올 수 있다.
기본적으로 properties파일을 통한 기능을 제공하고 있으며 추가적인 설정을 통해 DB의 테이블에서 property값을 가져오는 PropertySource를 제공하고 있다. 또한 사용자가 추가로 PropertySource를 정의할 수 있다.
bean을 정의할 때 ${…}의 내용을 property placeholder를 이용하여 대체할 수 있었다.
해당 코드는 다음과 같다.
<context:property-placeholder location="com/bank/config/datasource.properties"/>
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
<property name="driverClass" value="${database.driver}"/>
<property name="jdbcUrl" value="${database.url}"/>
<property name="username" value="${database.username}"/>
<property name="password" value="${database.password}"/>
</bean>
Spring 3.1이전에는 <context:property-placeholder>를 정의하면 PropertyPlaceholderConfigurer를 사용하였다. 그러나 Spring 3.1이후부터 PropertySourcesPlaceholderConfigurer를 내부에서 사용하고 있으며 위에서 ${database.*}값을 datasource.properties에서 찾지 못하면 Environment의 Property를 사용하도록 하고 있다.
PropertySource는 Environment를 통해 접근 가능하다. 즉, 사용자가 정의한 PropertySource 또한 Spring 3.1부터 property-placeholder를 통해 사용할 수 있는 것이다.
Spring 3.1에서는 사용자가 직접 PropertySource를 정의할 수 있는 방법을 제시한다. ApplicationContextInitializer인터페이스와 web.xml에 contextInitializerClasses서블릿 컨텍스트 파라미터, Environment를 이용하여 정의가 가능하다.
ApplicationContextInitializer인터페이스를 구현하여 ApplicationContext초기화 로직을 직접 등록할 수 있으며 이 때 contextInitializerClasses 서블릿 컨텍스트 파라미터에 이 구현클래스를 등록한다.
web.xml의 정의 예이다.
<context-param>
<param-name>contextInitializerClasses</param-name>
<param-value>com.bank.MyInitializer</param-value>
</context-param>
이 때 ApplicationContextInitializer 구현 클래스인 MyInitializer 예이다.
public class MyInitializer implements ApplicationContextInitializer<ConfigurableWebApplicationContext> {
public void initialize(ConfigurableWebApplicationContext ctx) {
PropertySource ps = new MyPropertySource();
ctx.getEnvironment().getPropertySources().addFirst(ps);
// perform any other initialization of the context ...
}
}
위와 같이 등록하면 ApplicationContext가 로딩되거나 refresh되는 시점에 ApplicationContextInitializer구현체가 동작한다. ApplicationContextInitializer구현체의 initialize메소드에서 사용자가 정의한 PropertySource(PropertySource를 상속)를 Environment내부의 propertySources에 (getPropertySources함수를 이용하여) 추가한다.
사용자 정의 PropertySource를 등록하면 Environment를 통해 직접 Property를 가져오는 방법이나 Property-placeholder를 통해서 모두 해당 Property를 가져올 수 있다.
전자정부 3.0에서는 DB의 테이블에서 Property값을 가져오는 DBPropertySource를 제공하고 있다.
DB기반의 PropertySource를 적용하기 위해서는 다음과 같이 설정한다.
WAS가 기동될 때 DB를 연결하여 테이블에서 property값들을 가져올 수 있도록 하는 xml을 설정한다. DB property값을 담을 table을 만든다. 이 때, 칼럼명은 PKEY, PVALUE로 만들도록 한다.
CREATE TABLE PROPERTY (
PKEY VARCHAR(20) NOT NULL PRIMARY KEY ,
PVALUE VARCHAR(20) NOT NULL
);
commit;
INSERT INTO PROPERTY (PKEY, PVALUE) VALUES ('egov.test.sample01', 'db-property-sample01');
INSERT INTO PROPERTY (PKEY, PVALUE) VALUES ('egov.test.sample02', 'db-property-sample02');
...
commit;
다음은 db설정 xml의 예이다.
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:p="http://www.springframework.org/schema/p"
xmlns:jdbc="http://www.springframework.org/schema/jdbc"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.2.xsd
http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc-3.2.xsd">
<jdbc:embedded-database id="dataSource" type="HSQL">
<jdbc:script location="classpath:db/ddl.sql" />
<jdbc:script location="classpath:db/dml.sql" />
</jdbc:embedded-database>
<bean id="dbPropertySource" class="egovframework.rte.fdl.property.db.DbPropertySource">
<constructor-arg value="dbPropertySource"/>
<constructor-arg ref="dataSource"/>
<constructor-arg value="SELECT PKEY, PVALUE FROM PROPERTY"/>
</bean>
</beans>
WAS 기동 시에 DB값을 가져오도록 web.xml에 추가설정이 필요하다. egov에서 제공해주는 DBPropertySourceInitializer를 추가해주고, 앞에서 설정한 xml의 path를 설정해준다.
<context-param>
<param-name>contextInitializerClasses</param-name>
<param-value>egovframework.rte.fdl.property.db.initializer.DBPropertySourceInitializer</param-value>
</context-param>
<context-param>
<param-name>propertySourceConfigLocation</param-name>
<param-value>classpath:/initial/propertysource-context.xml</param-value>
</context-param>
xml에서 정의된 PropertySource를 사용하기 위해서 property-placeholder만 설정해주면 된다.
...
<context:property-placeholder/>
<!-- 메시지소스빈 설정 -->
<bean id="propertyTest" class="egov.sample.property.PropertyTest">
<property name="sample01" value="${egov.test.sample01}"/>
<property name="sample02" value="${egov.test.sample02}"/>
</bean>
...
코드상에서 PropertySource에 접근하기 위해서는 egov 3.0부터 제공하는 Environment abstraction을 이용한다. 자세한 내용은 Environment를 참조하도록 한다.
표준프레임워크 3.0부터는 (Spring 3.1부터) Environment interface를 제공한다.
Environment는 다음 기능의 접근을 제공한다.
Environment는 ApplicationContext를 통해서 접근이 가능하며 다음과 같이 가져올 수 있다.
ApplicationContext ctx = new GenericApplicationContext();
Environment env = ctx.getEnvironment();
Profile은 등록할 bean들이 정의되어있는 논리적인 그룹을 말한다. Bean은 XML또는 Annotation을 통해 정의된 Profile값 중 활성화된 Profile로 할당된다. 이 때 현재 사용하는 Profile을 활성화하는 것이 바로 Environment의 역할이다. 또한 Profile은 default값으로 설정이 되어있어야 한다.
Spring에서 Profile을 활성화 할 때 내부에서 Environment를 쓰고 있으며 활성화하는 방법은 코드상 변환, 명시적 설정, Annotation설정 등이 있다.
Environment를 이용하여 다음과 같이 사용할 경우 Profile이 dev로 정의되어있는 bean들이 활성화된다.
GenericXmlApplicationContext ctx = new GenericXmlApplicationContext();
ctx.getEnvironment().setActiveProfiles("dev");
ctx.load("classpath:/com/bank/config/xml/*-config.xml");
ctx.refresh();
혹은 여러 개의 프로파일을 활성화 시킬 수도 있다.
ctx.getEnvironment().setActiveProfiles("profile1", "profile2");
web.xml의 servlet 설정에서 다음과 같이 명시적으로 쓸 수도 있다. 다음의 경우 profile이 production으로 정의되어있는 bean들이 활성화된다.
<servlet>
<servlet-name>dispatcher</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>spring.profiles.active</param-name>
<param-value>production</param-value>
</init-param>
</servlet>
Environment abstract에서는 PropertySource의 계층구조를 통해 통합검색 기능을 지원하고 있다.
다음은 Environment를 통해 PropertySource에 접근할 수 있는 예를 보여준다.
ApplicationContext ctx = new GenericApplicationContext();
Environment env = ctx.getEnvironment();
boolean containsFoo = env.containsProperty("foo");
System.out.println("Does my environment contain the 'foo' property? " + containsFoo);
PropertySource에서는 key-value쌍의 속성값에 접근할 수 있도록 기능을 제공하며 기본(default) Environment에는 두 개의 PropertySource객체로 구성되어있다.
Environment에서 PropertySource를 Hierarchical(계층구조)의 우선순위대로 검색한다. 위의 default environment에서 JVM 시스템 properties가 시스템 환경변수보다 우선순위가 높으므로 JVM 시스템 Properties에서 검색을 하고 다음 시스템 환경변수에서 Property값을 찾을 것이다.
위의 두 기본 PropertySource외에 사용자 정의 PropertySource를 만들 수 있다.
다음은 사용자가 정의한 MyPropertySource를 Environment의 Hierarchical PropertySource에 추가하기 위한 코드이다.
ConfigurableApplicationContext ctx = new GenericApplicationContext();
MutablePropertySources sources = ctx.getEnvironment().getPropertySources();
sources.addFirst(new MyPropertySource());
위의 코드에서 MyPropertySource는 가장 높은 우선순위로 Environment에 추가된다. 만약 이전 코드와 같이 “foo”의 Property값을 가져오는 경우, MyPropertySource에 값이 있다면 그 값이 반환될 것이다.
전자정부 프레임워크에서 Cache Service는 EhCache를 선정하여 가이드한다.
Spring 버전 3.1 이전에서는 EhCache에서 제공하는 CacheManager를 직접 사용한다. 3.1 이후 버전에서는 CacheManager Abstraction을 제공함으로써 Cache를 유연하게 사용할 수 있게 되었다. 아래에서는 EhCache의 설명과 Spring 3.1 이전의 EhCache 사용법에 대하여 알아본다.
EhCache를 이용하기 위한 기본 설정 및 기본 사용법에 대해서 설명한다.
Cache를 사용하기 위해서 Cache Manager를 생성하는 방법을 샘플을 통해서 설명한다.
//클래스 패스를 이용하여 설정파일 읽어서 Cache Manager 생성하기.
URL url = getClass().getResource("/ehcache-default.xml");
manager = new CacheManager(url);
위에서 getResource를 통해서 읽어들이는 /ehcache-default.xml 의 파일 내용은 다음과 같다.
<ehcache>
<diskStore path="user.dir/second"/>
<cache name="sampleMem"
maxElementsInMemory="3"
eternal="false"
timeToIdleSeconds="360"
timeToLiveSeconds="1000"
overflowToDisk="false"
diskPersistent="false"
memoryStoreEvictionPolicy="LRU">
</cache>
</ehcache>
위에서 정의한 Cache Manager에서 Cache를 얻어서 기본적인 쓰고 읽고 지우는 것에 대한 샘플은 다음과 같다.
// cache Name을 가지고 cache 얻기
Cache cache = manager.getCache("sampleMem");
// 1.Cache에 데이터 입력
cache.put(new Element("key1", "value1"));
// 2.Cache로부터 입력한 데이터 읽기
Element value = cache.get("key1");
// 3. Cache에서 데이터 삭제
cache.remove("key1");
Cache는 메모리를 이용하는 것을 기본으로 하기에 꼭 필요한 자료만을 관리하도록 보관 사이즈를 지정한다. 보관 사이즈를 넘어설 경우 불필요한 자료부터 삭제처리하는데 필요한 자료에 대한 판단은 알고리즘을 통해서 한다. 지원되는 알고리즘은 LRU, FIFO, LFU 세가지 이고 각각에 대한 설정및 사용법은 아래와 같다.
최근에 이용한 것을 남기는 알고리즘으로 설정은 아래와 같이 한다.
<cache name="sampleMem"
maxElementsInMemory="3"
...
memoryStoreEvictionPolicy="LRU">
위의 설정에서 최대 보관 엔트리 갯수(maxElementsInMemory)는 3, 알고리즘은 LRU 로 설정된 것을 확인할 수 있다. 이 설정을 이용하여 설정에 대한 결과를 확인하는 소스는 다음과 같다.
Cache cache = manager.getCache("sampleMem");
...
cache.get("key2");
cache.get("key2");
cache.get("key1");
cache.get("key1");
cache.get("key3");
// 4. Put New element in cache.
cache.put(new Element("key4", "value4"));
// 5. get key2 but can't key2
assertNull("Can't get key2",cache.get("key2"));
먼저 입력된것을 제거하는 알고리즘으로 설정은 아래와 같이 한다.
<cache name="sampleMemFIFO"
maxElementsInMemory="3"
...
memoryStoreEvictionPolicy="FIFO">
위의 설정에서 최대 보관 엔트리 갯수(maxElementsInMemory)는 3, 알고리즘은 FIFO 로 설정된 것을 확인할 수 있다. 이 설정을 이용하여 설정에 대한 결과를 확인하는 소스는 다음과 같다.
Cache cache = manager.getCache("sampleMemFIFO");
cache.put(new Element("key1", "value1"));
cache.put(new Element("key2", "value2"));
cache.put(new Element("key3", "value3"));
...
cache.get("key2");
cache.get("key2");
cache.get("key1");
cache.get("key1");
cache.get("key3");
// 4. Put New element in cache.
cache.put(new Element("key4", "value4"));
// 5. get key1 but can't key1
assertNull("Can't get key1",cache.get("key1"));
가장 적게 이용된 것을 제거하는 알고리즘으로 설정은 아래와 같이 한다.
<cache name="sampleMemLFU"
maxElementsInMemory="3"
...
memoryStoreEvictionPolicy="LFU">
위의 설정에서 최대 보관 엔트리 갯수(maxElementsInMemory)는 3, 알고리즘은 LFU 로 설정된 것을 확인할 수 있다. 이 설정을 이용하여 설정에 대한 결과를 확인하는 소스는 다음과 같다.
Cache cache = manager.getCache("sampleMemLFU");
cache.put(new Element("key1", "value1"));
cache.put(new Element("key2", "value2"));
cache.put(new Element("key3", "value3"));
...
cache.get("key2");
cache.get("key2");
cache.get("key1");
cache.get("key1");
cache.get("key3");
// 4. Put New element in cache.
cache.put(new Element("key4", "value4"));
// 5. get key2 but can't key3
assertNull("Can't get key3",cache.get("key3"));
Cache에서 관리해야 하는 정보의 사이즈 설정 및 저장 디바이스 관련 설정을 할 수 있다.
디바이스 관련 세팅은 메모리 관리 cache의 디스크 관리로의 이동관련 설정으로 메모리 관리 오브젝트 수가 넘었을때 Disk로 이동여부, flush 호출시 파일로 저장 여부에 대한 설정이 있다. 이에 대한 설정 예시는 다음과 같다.
<cache name="sampleDisk"
overflowToDisk="true"
diskPersistent="true"
...>
</cache>
이 설정을 이용하여 설정에 대한 결과를 확인하는 소스는 다음과 같다.
Ehcache cache = manager.getCache("sampleDisk");
// 1. Put a content in Cache.
cache.put(new Element("key1", "value1"));
// 2. Get that item from Cache.
Element value = cache.get("key1");
assertEquals("value1", value.getValue().toString());
// 3. flush 를 한다.
cache.flush();
File dataFile = new File(manager.getDiskStorePath()+ File.separator + "sampleDisk.data");
// 4. 파일로 저장 확인
assertTrue("File exists", dataFile.exists());
위의 샘플에서 flush 수행시 파일로 저장되는 것을 확인 할 수 있고, 메모리 관리 오브젝트 Disk이동 여부는 아래의 Cache Size의 예에서 확인한다.
사이즈 관련 설정은 메모리에서 관리해야 할 최대 오브젝트 수, 디스크에서 관리하는 최대 오브젝트 수에 대한 설정이 있다. 이에 대한 설정 예시는 다음과 같다.
<cache name="sampleDisk"
maxElementsInMemory="3"
maxElementsOnDisk="2"
overflowToDisk="true"
...>
</cache>
이 설정을 이용하여 설정에 대한 결과를 확인하는 소스는 다음과 같다.
Cache cache = manager.getCache("sampleDisk");
// 1. Put 3 contents in Cache.
cache.put(new Element("key1", "value1"));
cache.put(new Element("key2", "value2"));
cache.put(new Element("key3", "value3"));
// 2. Put Fourth content in Cache.
cache.put(new Element("key4", "value4"));
assertEquals(3, cache.getMemoryStoreSize()); //Memory에는 세개 유지함
assertEquals(1, cache.getDiskStoreSize()); //Disk로 하나 넘어감.
// 3. Put 5~7 contens in Cache.
cache.put(new Element("key5", "value5"));
cache.put(new Element("key6", "value6"));
cache.put(new Element("key7", "value7"));
// Disk Max Size에 관계 없이 모두 넘어감
assertEquals(3, cache.getMemoryStoreSize()); //Memory에는 세개 유지함
assertEquals(4, cache.getDiskStoreSize()); //Disk에는 2개를 넘어서서 4개 유지함.
// 메모리것을 디스크로 이동시킴
cache.flush();
// Disk의 Max Size 대로 변경됨.
assertEquals(0, cache.getMemoryStoreSize()); //Memory에는 없어짐.
assertEquals(2, cache.getDiskStoreSize()); //Disk에는 DiskMaxSize 대로 2개만 남김.
위의 예를 보면 cache.put에 의해서는 Disk의 MaxSize에 관계없이 계속 Memory에서 Disk로 넘어가지만 flush를 수행하면 최대 디스크 보관수만을 남기는 것을 확인 할 수 있다.
Ehcache는 Distributed Cache를 지원하는 방법으로 RMI,JGROUP,JMS등을 지원한다. 그 중에서 JGROUP와 ActiveMQ를 이용한 JMS 지원 설정 및 사용 방법을 설명한다. 자세한 설명은 Ehcache Documentation 참고.
JGroups는 multicast 기반의 커뮤니케이션 툴킷으로 그룹을 생성하고 그룹멤버간에 메세지를 주고 받을 수 있도록 지원한다. 관련한 자세한 정보는 사이트참조
<cacheManagerPeerProviderFactory class="net.sf.ehcache.distribution.jgroups.JGroupsCacheManagerPeerProviderFactory"
properties="connect=UDP(mcast_addr=224.10.10.10;mcast_port=5555;ip_ttl=32;
mcast_send_buf_size=150000;mcast_recv_buf_size=80000):
PING(timeout=2000;num_initial_members=6):
MERGE2(min_interval=5000;max_interval=10000):
FD_SOCK:VERIFY_SUSPECT(timeout=1500):
pbcast.NAKACK(gc_lag=10;retransmit_timeout=3000):
UNICAST(timeout=5000):
pbcast.STABLE(desired_avg_gossip=20000):
FRAG:
pbcast.GMS(join_timeout=5000;join_retry_timeout=2000;shun=false;print_local_addr=false)"
propertySeparator="::"/>
<cache name="cacheSync"
maxElementsInMemory="1000"
eternal="false"
timeToIdleSeconds="1000"
timeToLiveSeconds="1000"
overflowToDisk="false">
<cacheEventListenerFactory class="net.sf.ehcache.distribution.jgroups.JGroupsCacheReplicatorFactory"
properties="replicateAsynchronously=false, replicatePuts=true,
replicateUpdates=true, replicateUpdatesViaCopy=false,
replicateRemovals=true"/>
</cache>
이 설정을 이용하여 설정에 대한 결과를 확인하는 소스는 다음과 같다.
// 위의 설정파일을 정보 읽어오기
URL url = this.getClass().getResource("/ehcache-distributed-jgroups.xml");
// 두개의 Cache Manager 생성
manager1 = new CacheManager(url);
manager2 = new CacheManager(url);
for (int i = 0; i < 10 ; i++) {
manager1.getEhcache(CACHE_SYNC).put(new Element(new Integer(i), "value"));
}
// 리플리케이션을 위한 시간 필요함.
Thread.currentThread().sleep(100);
// 리플리케이션이 되어 manager2에도 동일한 Cache 정보 입력됨 확인.
assertTrue(manager1.getEhcache(CACHE_SYNC).getKeys().size() == manager2.getEhcache(CACHE_SYNC).getKeys().size() );
ActiveMQ는 JMS 메세징 서비스를 제공하는 오픈 소스이다. 관련한 자세한 정보는 사이트 참조
<cacheManagerPeerProviderFactory
class="net.sf.ehcache.distribution.jms.JMSCacheManagerPeerProviderFactory"
properties="initialContextFactoryName=egovframework.rte.fdl.cache.distribute.TestActiveMQInitialContextFactory,
providerURL=tcp://localhost:61616,
replicationTopicConnectionFactoryBindingName=topicConnectionFactory,
getQueueConnectionFactoryBindingName=queueConnectionFactory,
replicationTopicBindingName=ehcache,
getQueueBindingName=ehcacheGetQueue"
propertySeparator=","
/>
<cache name="CacheAsync"
maxElementsInMemory="1000"
eternal="false"
timeToIdleSeconds="1000"
timeToLiveSeconds="1000"
overflowToDisk="false">
<cacheEventListenerFactory class="net.sf.ehcache.distribution.jms.JMSCacheReplicatorFactory"
properties="replicateAsynchronously=true,
replicatePuts=true,
replicateUpdates=true,
replicateUpdatesViaCopy=true,
replicateRemovals=true,
asynchronousReplicationIntervalMillis=1000"
propertySeparator=","/>
</cache>
이 설정을 이용하여 설정에 대한 결과를 확인하는 소스는 다음과 같다.
URL url = this.getClass().getResource("/ehcache-distributed-activemq.xml");
manager1 = new CacheManager(url);
manager2 = new CacheManager(url);
cacheName = "CacheAsync";
for (int i = 0; i < 10 ; i++) {
manager1.getEhcache("CacheAsync").put(new Element(new Integer(i), "value"));
}
// replication 되는데 일정 시간이 필요함.
Thread.currentThread().sleep(1000);
assertTrue(manager1.getEhcache("CacheAsync").getKeys().size() == manager2.getEhcache("CacheAsync").getKeys().size() );
Spring 3.1 이전 버전에서 Ehcache를 이용하는 방법에 대해서 설정 및 설정을 이용한 기본 Cache 서비스에 대해서 설명한다.
<bean id="ehcache" class="org.springframework.cache.ehcache.EhCacheFactoryBean">
<property name="cacheManager">
<bean class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean">
<property name="configLocation" value="classpath:ehcache-default.xml"/>
</bean>
</property>
</bean>
위와 같이 설정하면 Manager를 통하지 않고서 cache를 얻을 수 있다. ConfigLocation에 정의된 ehcache-default.xml은 Cache Basic의 Configuration에 설명한 설정파일과 동일한 것이다.
@Resource(name="ehcache")
Ehcache gCache ;
// cache Name을 가지고 cache 찾기
Ehcache cache = gCache.getCacheManager().getCache("sampleMem");
cache.put(new Element("key1", "value1"));
Element value = cache.get("key1");
위의 예를 보면 ehcache를 이용하여 Ehcache를 가지고 오고 getCacheManager()를 통해서 이름을 통한 cache 정보를 읽어오는 것을 확인할 수 있다. 그 이후에는 위에서 설명한 것과 동일한 방식으로 사용하는 것을 확인 할 수 있다.
전자정부 프레임워크에서 Cache Service는 EhCache를 선정하여 가이드한다.
Spring 버전 3.1 이전에서는 EhCache에서 제공하는 CacheManager를 직접 사용한다. 3.1 이후 버전에서는 CacheManager Abstraction을 제공함으로써 Cache를 유연하게 사용할 수 있게 되었다. 아래에서는 EhCache의 설명과 Spring 3.1이전의 EhCache 사용법에 대하여 알아본다.
EhCache를 이용하기 위한 기본 설정 및 기본 사용법에 대해서 설명한다.
Cache를 사용하기 위해서 Cache Manager를 생성하는 방법을 샘플을 통해서 설명한다.
//클래스 패스를 이용하여 설정파일 읽어서 Cache Manager 생성하기.
URL url = getClass().getResource("/ehcache-default.xml");
manager = new CacheManager(url);
위에서 getResource를 통해서 읽어들이는 /ehcache-default.xml 의 파일 내용은 다음과 같다.
<ehcache>
<diskStore path="user.dir/second"/>
<cache name="sampleMem"
maxElementsInMemory="3"
eternal="false"
timeToIdleSeconds="360"
timeToLiveSeconds="1000"
overflowToDisk="false"
diskPersistent="false"
memoryStoreEvictionPolicy="LRU">
</cache>
</ehcache>
위에서 정의한 Cache Manager에서 Cache를 얻어서 기본적인 쓰고 읽고 지우는 것에 대한 샘플은 다음과 같다.
// cache Name을 가지고 cache 얻기
Cache cache = manager.getCache("sampleMem");
// 1.Cache에 데이터 입력
cache.put(new Element("key1", "value1"));
// 2.Cache로부터 입력한 데이터 읽기
Element value = cache.get("key1");
// 3. Cache에서 데이터 삭제
cache.remove("key1");
Cache는 메모리를 이용하는 것을 기본으로 하기에 꼭 필요한 자료만을 관리하도록 보관 사이즈를 지정한다. 보관 사이즈를 넘어설 경우 불필요한 자료부터 삭제처리하는데, 필요한 자료에 대한 판단은 알고리즘을 통해서 한다. 지원되는 알고리즘은 LRU, FIFO, LFU 세 가지이고 각각에 대한 설정 및 사용법은 아래와 같다.
최근에 이용한 것을 남기는 알고리즘으로 설정은 아래와 같이 한다.
<cache name="sampleMem"
maxElementsInMemory="3"
...
memoryStoreEvictionPolicy="LRU">
위의 설정에서 최대 보관 엔트리 갯수(maxElementsInMemory)는 3, 알고리즘은 LRU 로 설정된 것을 확인할 수 있다. 이 설정을 이용하여 설정에 대한 결과를 확인하는 소스는 다음과 같다.
Cache cache = manager.getCache("sampleMem");
...
cache.get("key2");
cache.get("key2");
cache.get("key1");
cache.get("key1");
cache.get("key3");
// 4. Put New element in cache.
cache.put(new Element("key4", "value4"));
// 5. get key2 but can't key2
assertNull("Can't get key2",cache.get("key2"));
먼저 입력된것을 제거하는 알고리즘으로 설정은 아래와 같이 한다.
<cache name="sampleMemFIFO"
maxElementsInMemory="3"
...
memoryStoreEvictionPolicy="FIFO">
위의 설정에서 최대 보관 엔트리 갯수(maxElementsInMemory)는 3, 알고리즘은 FIFO 로 설정된 것을 확인할 수 있다. 이 설정을 이용하여 설정에 대한 결과를 확인하는 소스는 다음과 같다.
Cache cache = manager.getCache("sampleMemFIFO");
cache.put(new Element("key1", "value1"));
cache.put(new Element("key2", "value2"));
cache.put(new Element("key3", "value3"));
...
cache.get("key2");
cache.get("key2");
cache.get("key1");
cache.get("key1");
cache.get("key3");
// 4. Put New element in cache.
cache.put(new Element("key4", "value4"));
// 5. get key1 but can't key1
assertNull("Can't get key1",cache.get("key1"));
가장 적게 이용된 것을 제거하는 알고리즘으로 설정은 아래와 같이 한다.
<cache name="sampleMemLFU"
maxElementsInMemory="3"
...
memoryStoreEvictionPolicy="LFU">
위의 설정에서 최대 보관 엔트리 갯수(maxElementsInMemory)는 3, 알고리즘은 LFU 로 설정된 것을 확인할 수 있다. 이 설정을 이용하여 설정에 대한 결과를 확인하는 소스는 다음과 같다.
Cache cache = manager.getCache("sampleMemLFU");
cache.put(new Element("key1", "value1"));
cache.put(new Element("key2", "value2"));
cache.put(new Element("key3", "value3"));
...
cache.get("key2");
cache.get("key2");
cache.get("key1");
cache.get("key1");
cache.get("key3");
// 4. Put New element in cache.
cache.put(new Element("key4", "value4"));
// 5. get key2 but can't key3
assertNull("Can't get key3",cache.get("key3"));
Cache에서 관리해야 하는 정보의 사이즈 설정 및 저장 디바이스 관련 설정을 할 수 있다.
디바이스 관련 설정은 메모리 관리 cache의 디스크 관리로의 이동관련 설정으로 메모리 관리 오브젝트 수가 넘었을때 Disk로 이동여부, flush 호출시 파일로 저장 여부에 대한 설정이 있다. 이에 대한 설정 예시는 다음과 같다.
<cache name="sampleDisk"
overflowToDisk="true"
diskPersistent="true"
...>
</cache>
true, false)true, false)이 설정을 이용하여 설정에 대한 결과를 확인하는 소스는 다음과 같다.
Ehcache cache = manager.getCache("sampleDisk");
// 1. Put a content in Cache.
cache.put(new Element("key1", "value1"));
// 2. Get that item from Cache.
Element value = cache.get("key1");
assertEquals("value1", value.getValue().toString());
// 3. flush 를 한다.
cache.flush();
File dataFile = new File(manager.getDiskStorePath()+ File.separator + "sampleDisk.data");
// 4. 파일로 저장 확인
assertTrue("File exists", dataFile.exists());
위의 샘플에서 flush 수행시 파일로 저장되는 것을 확인 할 수 있고, 메모리 관리 오브젝트 Disk이동 여부는 아래의 Cache Size의 예에서 확인한다.
사이즈 관련 설정은 메모리에서 관리해야 할 최대 오브젝트 수, 디스크에서 관리하는 최대 오브젝트 수에 대한 설정이 있다. 이에 대한 설정 예시는 다음과 같다.
<cache name="sampleDisk"
maxElementsInMemory="3"
maxElementsOnDisk="2"
overflowToDisk="true"
...>
</cache>
이 설정을 이용하여 설정에 대한 결과를 확인하는 소스는 다음과 같다.
Cache cache = manager.getCache("sampleDisk");
// 1. Put 3 contents in Cache.
cache.put(new Element("key1", "value1"));
cache.put(new Element("key2", "value2"));
cache.put(new Element("key3", "value3"));
// 2. Put Fourth content in Cache.
cache.put(new Element("key4", "value4"));
assertEquals(3, cache.getMemoryStoreSize()); //Memory에는 세개 유지함
assertEquals(1, cache.getDiskStoreSize()); //Disk로 하나 넘어감.
// 3. Put 5~7 contens in Cache.
cache.put(new Element("key5", "value5"));
cache.put(new Element("key6", "value6"));
cache.put(new Element("key7", "value7"));
// Disk Max Size에 관계 없이 모두 넘어감
assertEquals(3, cache.getMemoryStoreSize()); //Memory에는 세개 유지함
assertEquals(4, cache.getDiskStoreSize()); //Disk에는 2개를 넘어서서 4개 유지함.
// 메모리것을 디스크로 이동시킴
cache.flush();
// Disk의 Max Size 대로 변경됨.
assertEquals(0, cache.getMemoryStoreSize()); //Memory에는 없어짐.
assertEquals(2, cache.getDiskStoreSize()); //Disk에는 DiskMaxSize 대로 2개만 남김.
위의 예를 보면 cache.put에 의해서는 Disk의 MaxSize에 관계없이 계속 Memory에서 Disk로 넘어가지만 flush를 수행하면 최대 디스크 보관수만을 남기는 것을 확인 할 수 있다.
Ehcache는 Distributed Cache를 지원하는 방법으로 RMI, JGROUP, JMS등을 지원한다. 그 중에서 JGROUP와 ActiveMQ를 이용한 JMS 지원 설정 및 사용 방법을 설명한다. 자세한 설명은 Ehcache Documentation 참고.
JGroups는 multicast 기반의 커뮤니케이션 툴킷으로 그룹을 생성하고 그룹멤버간에 메세지를 주고 받을 수 있도록 지원한다. 관련한 자세한 정보는 사이트 참고
<cacheManagerPeerProviderFactory class="net.sf.ehcache.distribution.jgroups.JGroupsCacheManagerPeerProviderFactory"
properties="connect=UDP(mcast_addr=224.10.10.10;mcast_port=5555;ip_ttl=32;
mcast_send_buf_size=150000;mcast_recv_buf_size=80000):
PING(timeout=2000;num_initial_members=6):
MERGE2(min_interval=5000;max_interval=10000):
FD_SOCK:VERIFY_SUSPECT(timeout=1500):
pbcast.NAKACK(gc_lag=10;retransmit_timeout=3000):
UNICAST(timeout=5000):
pbcast.STABLE(desired_avg_gossip=20000):
FRAG:
pbcast.GMS(join_timeout=5000;join_retry_timeout=2000;shun=false;print_local_addr=false)"
propertySeparator="::"/>
<cache name="cacheSync"
maxElementsInMemory="1000"
eternal="false"
timeToIdleSeconds="1000"
timeToLiveSeconds="1000"
overflowToDisk="false">
<cacheEventListenerFactory class="net.sf.ehcache.distribution.jgroups.JGroupsCacheReplicatorFactory"
properties="replicateAsynchronously=false, replicatePuts=true,
replicateUpdates=true, replicateUpdatesViaCopy=false,
replicateRemovals=true"/>
</cache>
이 설정을 이용하여 설정에 대한 결과를 확인하는 소스는 다음과 같다.
// 위의 설정파일을 정보 읽어오기
URL url = this.getClass().getResource("/ehcache-distributed-jgroups.xml");
// 두개의 Cache Manager 생성
manager1 = new CacheManager(url);
manager2 = new CacheManager(url);
for (int i = 0; i < 10 ; i++) {
manager1.getEhcache(CACHE_SYNC).put(new Element(new Integer(i), "value"));
}
// 리플리케이션을 위한 시간 필요함.
Thread.currentThread().sleep(100);
// 리플리케이션이 되어 manager2에도 동일한 Cache 정보 입력됨 확인.
assertTrue(manager1.getEhcache(CACHE_SYNC).getKeys().size() == manager2.getEhcache(CACHE_SYNC).getKeys().size() );
ActiveMQ는 JMS 메세징 서비스를 제공하는 오픈 소스이다. 관련한 자세한 정보는 사이트 참조
<cacheManagerPeerProviderFactory
class="net.sf.ehcache.distribution.jms.JMSCacheManagerPeerProviderFactory"
properties="initialContextFactoryName=egovframework.rte.fdl.cache.distribute.TestActiveMQInitialContextFactory,
providerURL=tcp://localhost:61616,
replicationTopicConnectionFactoryBindingName=topicConnectionFactory,
getQueueConnectionFactoryBindingName=queueConnectionFactory,
replicationTopicBindingName=ehcache,
getQueueBindingName=ehcacheGetQueue"
propertySeparator=","
/>
<cache name="CacheAsync"
maxElementsInMemory="1000"
eternal="false"
timeToIdleSeconds="1000"
timeToLiveSeconds="1000"
overflowToDisk="false">
<cacheEventListenerFactory class="net.sf.ehcache.distribution.jms.JMSCacheReplicatorFactory"
properties="replicateAsynchronously=true,
replicatePuts=true,
replicateUpdates=true,
replicateUpdatesViaCopy=true,
replicateRemovals=true,
asynchronousReplicationIntervalMillis=1000"
propertySeparator=","/>
</cache>
이 설정을 이용하여 설정에 대한 결과를 확인하는 소스는 다음과 같다.
URL url = this.getClass().getResource("/ehcache-distributed-activemq.xml");
manager1 = new CacheManager(url);
manager2 = new CacheManager(url);
cacheName = "CacheAsync";
for (int i = 0; i < 10 ; i++) {
manager1.getEhcache("CacheAsync").put(new Element(new Integer(i), "value"));
}
// replication 되는데 일정 시간이 필요함.
Thread.currentThread().sleep(1000);
assertTrue(manager1.getEhcache("CacheAsync").getKeys().size() == manager2.getEhcache("CacheAsync").getKeys().size() );
Spring 3.1 이전 버전에서 Ehcache를 이용하는 방법에 대해서 설정 및 설정을 이용한 기본 Cache 서비스에 대해서 설명한다.
<bean id="ehcache" class="org.springframework.cache.ehcache.EhCacheFactoryBean">
<property name="cacheManager">
<bean class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean">
<property name="configLocation" value="classpath:ehcache-default.xml"/>
</bean>
</property>
</bean>
위와 같이 설정하면 Manager를 통하지 않고서 cache를 얻을 수 있다. ConfigLocation에 정의된 ehcache-default.xml은 Cache Basic의 Configuration에 설명한 설정파일과 동일한 것이다.
@Resource(name="ehcache")
Ehcache gCache ;
// cache Name을 가지고 cache 찾기
Ehcache cache = gCache.getCacheManager().getCache("sampleMem");
cache.put(new Element("key1", "value1"));
Element value = cache.get("key1");
위의 예를 보면 ehcache를 이용하여 Ehcache를 가지고 오고 getCacheManager()를 통해서 이름을 통한 cache 정보를 읽어오는 것을 확인할 수 있다. 그 이후에는 위에서 설명한 것과 동일한 방식으로 사용하는 것을 확인 할 수 있다.
Spring 3.1부터 Cache Service는 Cache 추상화(CacheManager Interface)와 Cache 추상화를 Java메소드에 제공할 수 있는 @Cacheable을 제공한다. Cache 추상화는 Spring의 트랜잭션기능과 유사하게 코드의 변화를 최소화하면서 Proxy를 통해 동작하게끔 한다. Cache 구현체가 아닌 Cache추상화만을 제공하며 실제 Cache Data저장소는 EhCache와 ConcurrentMap을 지원한다.
Cache를 설정하여 CacheManager를 통해 Cache에 접근하는 방법에 대하여 알아보고, 자바메소드를 Caching하는 @Cacheable에 대하여 알아본다.
Spring에서는 EhCache를 지원하는 CacheManager로써 EhCacheCacheManager를 제공한다. <EhCache 설정>
<cache:annotation-driven cache-manager="cacheManager" />
<!-- EhCache를 저장소로 사용하는 Cache Manager -->
<bean id="cacheManager" class="org.springframework.cache.ehcache.EhCacheCacheManager">
<property name="cacheManager" ref="ehcache"></property>
</bean>
<!-- Ehcache library setup -->
<bean id="ehcache"
class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean"
p:config-location="classpath:springframework/cache/ehcache/ehcache.xml" />
Ehcache.xml에서 defaultCache저장소와 추가 Cache저장소의 설정을 한다. EhCache는 비록 ConcurrentMap보다 속도는 느리지만 Cache관리기능 측면에서 유용하여 EhCache를 추천한다.
<ehcache.xml>
<ehcache…>
<defaultCache
maxElementsInMemory="10000"
eternal="false"
timeToIdleSeconds="120"
timeToLiveSeconds="120"
overflowToDisk="true"
diskSpoolBufferSizeMB="30"
maxElementsOnDisk="10000000"
diskPersistent="false"
diskExpiryThreadIntervalSeconds="120"
memoryStoreEvictionPolicy="LRU"
statistics="false"
/>
<cache name="ehcache"
…>
Spring에서는 ConcurrentMap을 지원하는 SimpleCacheManager를 제공한다.
<CacheManager설정>
<cache:annotation-driven cache-manager="cacheManager"/>
<!-- ConcurrentMap을 저장소로 사용하는 Cache Manager -->
<bean id="cacheManager" class="org.springframework.cache.support.SimpleCacheManager">
<property name="caches">
<set>
<bean class="org.springframework.cache.concurrent.ConcurrentMapCacheFactoryBean" p:name="default"/>
<bean class="org.springframework.cache.concurrent.ConcurrentMapCacheFactoryBean" p:name="books"/>
</set>
</property>
</bean>
Cache Annotation으로 Cache를 적용하기 위해서는 반드시 <cache:annotatioin-driven>을 붙여주어야한다. 이 네임스페이스는 AOP를 사용해서 캐시 기능을 다양한 방법으로 설정할 수 있는 옵션을 제공한다. 이 설정은 Transaction에 사용되는 <tx:annotation-driven>과 비슷하다.
| 속성 | 기본값 | 설명 |
|---|---|---|
| cache-manager | cacheManager | 사용할 CacheManager의 이름. CacheManager의 Bean id가 cacheManager가 아닐 경우, 설정해야한다. |
| mode | proxy | 스프링 AOP를 사용하는 설정이며, “aspect”를 사용할 수도 있다. |
| proxy-target-class | false | false인 경우, JDK의 인터페이스 기반 프록시를 사용한다. true를 사용하면 클래스 기반 프록시를 사용한다. |
| order | Ordered.LOWEST_PRECEDENCE | @Cacheable/@CacheEvict메소드의 Cache advice가 적용되는 순서 |
<cache:annotation-driven/>은 오로지 이것이 정의된 동일 ApplicationContext안의 bean에서 @Cacheable과 @CacheEvict을 찾는다. 즉, <cache:annotation-driven>을 DispatcherServlet을 위한 WebApplicationContext에 선언했을 때 @Cacheable/@CacheEvict는 controller내부에서만 식별된다.ehCache, concurrentMap의 설정과 상관없이 코드상으로 동일한 CacheManager인터페이스로 bean을 주입받아 사용할 수 있다.
<CacheManager 의 사용>
import org.springframework.cache.CacheManager;
@Autowired
private CacheManager cacheManager;
Cache cache = cacheManager.getCache("cache명");
자바 메소드에 @Cacheable을 설정함으로써 Caching할 수 있다. 타겟 메소드가 호출되었을 때, 캐시에 해당 메서드가 이미 동일한 인자로 있는지 확인하고, 만약 있다면 메소드를 호출하지 않고 캐시해둔 결과를 Proxy에서 반환하게 된다.
Java Method에 적용가능한 Cache Annotation은 다음과 같다.
@Cacheable@CacheEvict별다른 조건 없이 호출되는 모든 인자를 caching하고자 할 때는 아래와 같이 cache명만 쓰면 된다. 이 코드는 Cache이름이 “books”인 cache저장소를 사용하였다. 메소드가 호출될 때 매번 “books”에 Cache데이터를 확인하고 이미 실행된 적이 있는지를 확인한다. 만약 “books”에 데이터가 있으면 그 값을 반환하게 된다.
@Cacheable("books")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)
Cache되는 저장소를 여러 개 정의할 수도 있다. 아래의 코드에서는 findBook메소드 호출 시 “books”와 “isbns” 두 군데에 캐시데이터가 저장된다.
<여러 cache 저장소에 caching>
@Cacheable({ "books", "isbns" })
public Book findBook(ISBN isbn) {...}
Cache는 키-값으로 저장되며, 캐시된 메소드를 호출시마다 키를 통해 값을 가져오므로 캐시를 찾을 수 있는 키가 생성되어야 한다. 별도의 커스텀 키가 정의되지 않으면 default로 다음과 같은 알고리즘 기반의 KeyGenerator를 사용하여 Key를 생성한다.
이 외에 다른 기본키를 생성하려면 org.springframework.cache.KeyGenerator 인터페이스를 구현하면 된다.
@Cacheable을 적용한 메소드의 인자가 여러개일 때 Key로 사용할 것을 SpEL로 명시할 수 있다.
@Cacheable(value="books", key="#isbn"
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)
@Cacheable(value="books", key="#isbn.rawNumber")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)
@Cacheable(value="books", key="T(someType).hash(#isbn)")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)
Conditional을 주어서 그 값이 true이면 caching을 하고, false이면 caching을 하지 않기 때문에 호출 시 매번 메소드 내부가 실행된다. Conditional에서는 SpEL사용이 가능하며 Condition과 Unless를 쓸 수 있다. condition과는 달리 unless는 메소드의 결과값 반환 시점에 결과값을 확인하여 caching여부를 결정하게 된다.
@Cacheable(value="book", condition="#name.length < 32")
public Book findBook(String name)
@Cacheable(value="book", condition="#name.length < 32", unless="#result.hardback")
public Book findBook(String name)
Conditional에 쓰는 SpEL의 설명은 다음과 같다.
| 명칭 | 위치 | 설명 | 예제 |
|---|---|---|---|
| methodName | root객체 | 호출되는 메소드명 | #root.methodName |
| method | root객체 | 호출되는 메소드 | #root.method.name |
| target | root객체 | 호출되는 타겟 오브젝트 | #root.target |
| targetClass | root객체 | 호출되는 타겟 클래스 | #root.targetClass |
| args | root객체 | 타겟을 호출 시 사용되는 인자들(배열) | #root.args[0] |
| caches | root객체 | 현재 메소드가 실행되는 캐시들의 집합 | #root.caches[0].name |
| argument name | 평가 context | 메소드 인자명을 사용할 수 없을 때 대신 a<#arg>로 대체하여 사용할 수 있다. #arg는 0부터 시작하는 인자의 인덱스를 나타난다. | iban 또는 a0 (p<#arg>로도 사용가능) |
| result | 평가 context | 메소드 호출 결과. unless와 cache evict표현에서만 사용가능하다. | #result |
@CacheEvict는 @Cacheable과 반대로 cache저장소의 데이터를 제거함으로써 사용하지 않는 데이터를 정리하는데 유용하다. @CacheEvict는 캐시 삭제를 수행할 메서드에 선언한다. @CacheEvict도 여러 개의 캐시를 명시할 수 있으며, key와 condition을 사용할 수 있다. 또한 allEntries 속성은 키 값으로 Cache Entry 하나만 비우는 것이 아니라 캐시영역의 모든 Entries를 비우도록 한다. 이 경우에는 키를 명시하더라도 이를 무시하고 모든 Entries를 비우게 된다.
@CacheEvict(value = "books", allEntries=true)
public void loadBooks(InputStream batch)
메소드의 흐름을 방해하지 않고 Cache에 저장하거나 업데이트를 해야 하는 경우, @CachePut을 사용한다. 즉, 메소드는 항상 실행되고 그 결과가 캐시에 저장된다. @CachePut은 @Cacheable과 동일한 옵션을 제공하며, Cache에 저장하는 것보다는 메소드의 흐름을 최적화하는데 사용되어야 한다.
@Cacheable과 함께 사용하는 것은 일반적으로 권장하지 않는다.
다수의 Cache annotation을 쓰고자 할 때 @Caching을 쓴다. @Cacheable, @CacheEvic, @CachePut을 지원한다.
@Caching(evict = { @CacheEvict("primary"), @CacheEvict(value = "secondary", key = "#p0") })
public Book importBooks(String deposit, Date date)
Java메소드에 Annotation을 붙이는 대신 XML로 caching할 메소드를 정의할 수 있다. 아래에서는 CacheManager bean설정을 생략하였다. BookService 패키지의 하위 메소드 중 findBook 메소드에 cacheable이 적용되며 loadBooks 메소드에 cacheEvict가 적용되었다.
<!-- the service we want to make cacheable -->
<bean id="bookService" class="x.y.service.DefaultBookService"/>
<!-- cache definitions -->
<cache:advice id="cacheAdvice" cache-manager="cacheManager">
<cache:caching cache="books">
<cache:cacheable method="findBook" key="#isbn"/>
<cache:cache-evict method="loadBooks" all-entries="true"/>
</cache:caching>
</cache:advice>
<!-- apply the cacheable behavior to all BookService interfaces -->
<aop:config>
<aop:advisor advice-ref="cacheAdvice" pointcut="execution(* x.y.BookService.*(..))"/>
</aop:config>
...
// CacheManager설정 생략
Object/XML Mapping, 줄여서 O/X mapping은 Object를 XML문서로 변환하는데 이를 XML Mashalling 또는 Marshalling 이다. 반대로 XML문서를 Object로 변환하는 것은 Unmarshalling 이다.
Client <------ XML ------> Server
WS는 Server와 Client 두 대상 간의 데이터를 주고받는 기술 중 하나이다. 정보를 요청하는쪽이 Client이다.(Client는 Server가 될 수도 있고 일반 사용자가 될 수도 있다.) 요청한 정보를 받아서 알맞게 처리 후 결과값을 리턴하는 쪽이 Server이다.
Client(OXM) <------ XML(WSDL) ------> (OXM)Server
WS는 XML(WSDL)형식으로 데이터를 주고받는다. 따라서 이 XML를 객체화 하거나 객체를 XML화 해야 한다. 그것이 Marshalling,Unmarshalling이다. OXM Utill은 JAXB,Castor,XMLBeans,JiBX,XStream..등 여러 가지가 있다.
Spring의 모든 marshalling 추상 클래스들은 org.springframework.oxm.Marshaller interface를 implemention 한다.
public interface Marshaller {
/**
* Marshals the object graph with the given root into the provided Result.
*/
void marshal(Object graph, Result result)
throws XmlMappingException, IOException;
}
| PARAMETER | 설 명 |
|---|---|
| Object | XML문서구조와 같은 Java 객체 |
| javax.xml.transform.Result | XML 출력과 관련된 객체들이 반드시 구현해야 할 Interface |
| javax.xml.transform.Result | XML표현 |
|---|---|
| javax.xml.transform.dom.DOMResult | org.w3c.dom.Node |
| javax.xml.transform.sax.SAXResult | org.xml.sax.ContentHandler |
| javax.xml.transform.stream.StreamResult | java.io.OutputStream java.io.File 또는 java.io.Writer |
Spring의 모든 Unmarshalling 추상 클래스들은 org.springframework.oxm.Unmarshaller interface를 implemention 한다.
public interface Unmarshaller {
/**
* Unmarshals the given provided Source into an object graph.
*/
Object unmarshal(Source source)
throws XmlMappingException, IOException;
}
| PARAMETER | 설 명 |
|---|---|
| javax.xml.transform.Source | XML source or transformation instructions 등을 매개변수로 받는 StreamSource 객체 |
| javax.xml.transform.Source | XML표현 |
|---|---|
| javax.xml.transform.dom.DOMSource | org.w3c.dom.Node |
| javax.xml.transform.sax.SAXSource | org.xml.sax.InputSource org.xml.sax.XMLReader |
| javax.xml.transform.stream.StreamSource | java.io.File java.io.InputStream 또는 java.io.Reader |
Spring’s OXM은 다양한 Java-XML Binding 오픈소스를 지원한다. 여기서는 오픈소스 Castor와 XMLBeans를 사용하여 구현한 가이드프로그램을 제시한다.
Castor XML mapping은 XML Binding 오픈소스 프레임워크이다. Castor는 java object에서 XML문서, XML문서에서 java object로 변환을 지원한다. mapping file을 사용하여 좀더 수월하게 Castor를 사용할 수 있지만 그 외 추가적인 구성은 할 필요가 없다. 좀 더 많은 OpenSource Castor 정보를 원한다면 Castor web site와 org.springframework.oxm.castor package를 참조하면 된다.
| ARGS | 설 명 |
|---|---|
| text.xsd | 참조할 xml 스키마 |
| gen.xyz | xml 스키마를 사용하여 Generation한 Java class package |
<bean id="divertcastor" class="egovframework.rte.fdl.divert.DivertCastor">
<property name="marshaller" ref="castorMarshaller" />
<property name="unmarshaller" ref="castorMarshaller" />
</bean>
<bean id="castorMarshaller" class="org.springframework.oxm.castor.CastorMarshaller"/>
| PROPERTIES | 설 명 |
|---|---|
| marshaller | org.springframework.oxm.castor.CastorMarshaller |
| unmarshaller | org.springframework.oxm.castor.CastorMarshaller |
Java Object의 데이타를 XML문서로 DataBinding Sample Source
@Resource(name = "castorMarshaller")
private Marshaller marshaller;
public void testMarshalling()
{
try {
FileOutputStream os = null;
List<Writer> book2Writers = new ArrayList<Writer>();
book2Writers.add(new Writer("J,J.R 툴킨"));
book2Writers.add(new Writer("J.J.T 툴킨"));
BookMg bookMg2 = new BookMg("9780446618502", "반지의 제왕", book2Writers);
os = new FileOutputStream("CasterBook.xml");
marshaller.marshal(bookMg2, new StreamResult(os));
} catch (Exception e)
{
logger.debug(e.getMessage());
e.printStackTrace(System.err);
fail("testMarshalling failed!");
}
}
Writer.java
public class Writer
{
private String Name;
// 생성자
public Writer() { }
// 작가명
public Writer(String Name) {
this.Name = Name;
}
// 작가명 수정
public void setName(String Name) {
this.Name = Name;
}
// 작가명 리턴
public String getName() {
return Name;
}
}
BookMg.java
public class BookMg
{
private String isbn;
private String title;
private List<Writer> writers;
public BookMg() { }
// 생성자
@ isbn 책번호
@ title 책제목
@ writers 작가리스트
public BookMg(String isbn, String title, List<Writer> writers) {
this.isbn = isbn;
this.title = title;
this.writers = writers;
}
// 생성자
@ isbn 책번호
@ title 책제목
@ writer 작가
public BookMg(String isbn, String title, Writer writer) {
this.isbn = isbn;
this.title = title;
this.writers = new LinkedList<Writer>();
writers.add(writer);
}
// 책번호 설정
@ isbn 책번호
public void setIsbn(String isbn) {
this.isbn = isbn;
}
// 책번호 리턴
public String getIsbn() {
return isbn;
}
// 책제목 설정
@ title 책제목
public void setTitle(String title) {
this.title = title;
}
// 책제목 리턴
public String getTitle() {
return title;
}
// 작가리스트 설정
@ writers 작가리스트
public void setWriters(List<Writer> writers) {
this.writers = writers;
}
// 작가리스트 리턴
public List<Writer> getWriters() {
return writers;
}
// 작가리스트에 작가 추가
@ writer 작가
public void addWriter(Writer writer) {
writers.add(writer);
}
}
CasterBook.xml
<?xml version="1.0" encoding="UTF-8"?>
<book-mg>
<isbn>9780446618502</isbn>
<writers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="java:egovframework.rte.fdl.divert.Writer">
<name>J,J.R 툴킨</name>
</writers>
<writers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="java:egovframework.rte.fdl.divert.Writer">
<name>J.J.T 툴킨</name>
</writers>
<title>반지의 제왕2</title>
</book-mg>
**##### XML문서를 JavaObject로 DataBinding Sample Source
@Resource(name = "castorMarshaller")
private Unmarshaller unmarshaller;
@Test
public void testUnmarshalling()
{
FileInputStream is = null;
try
{
is = new FileInputStream("CasterBook.xml");
bookMg = (BookMg) unmarshaller.unmarshal(new StreamSource(is));
writers = bookMg.getWriters();
for (Iterator i = writers.iterator(); i.hasNext(); )
{
Writer writer = (Writer)i.next();
}
}catch(FileNotFoundException fnde)
{
fnde.getStackTrace();
}
catch(IOException ioe)
{
ioe.getStackTrace();
}
finally
{
if (is != null)
{
try
{
is.close();
}catch(IOException ioe)
{
ioe.getStackTrace();
}
}
}
}
XMLBeans는 스키마 기반으로 XML 인포셋 전체에 커서 기반으로 접근할 수 있도록 하는 XML-Java binding tool이다. BEA Systems에 의해 개발되었으며 2003년에 아파치 프로젝트에 기증 되었다. 좀 더 많은 정보는 XMLBeans web site 와 org.springframework.oxm.xmlbeans package를 참조 하면 된다.
| ARGS | 설 명 |
|---|---|
| text.jar | binding classes jar |
| text.xsd | 참조할 xml 스키마 |
<bean id="divertxmlbeans" class="egovframework.rte.fdl.divert.DivertXMLBeans">
<property name="marshaller" ref="xmlBeansMarshaller" />
<property name="unmarshaller" ref="xmlBeansMarshaller" />
</bean>
<bean id="xmlBeansMarshaller" class="org.springframework.oxm.xmlbeans.XmlBeansMarshaller" />
| PROPERTIES | 설 명 |
|---|---|
| marshaller | org.springframework.oxm.xmlbeans.XmlBeansMarshaller |
| unmarshaller | org.springframework.oxm.xmlbeans.XmlBeansMarshaller |
Java Object의 데이터를 XML문서로 DataBinding Sample Source
@Resource(name = "xmlBeansMarshaller")
private Marshaller marshaller;
@Test
public void testMarshalling()
{
FileOutputStream os = null;
userDoc = UserinfoDocument.Factory.newInstance();
userElement = userDoc.addNewUserinfo();
userElement.setName("홍길동");
userElement.setAge(31);
userElement.setPhone(022770918);
xmlOptions = new XmlOptions();
xmlOptions.setSavePrettyPrint();
xmlOptions.setSavePrettyPrintIndent(4);
xmlOptions.setCharacterEncoding("euc-kr");
try
{
os = new FileOutputStream("XMLBeanGen.xml");
marshaller.marshal(userDoc, new StreamResult(os));
}catch(Exception ee)
{
ee.getStackTrace();
fail("testMarshalling failed!");
}
finally
{
if (os != null)
{
try
{
os.close();
}catch(IOException e3)
{
e3.getStackTrace();
fail("testMarshalling failed!");
}
}
}
}
XMLBeanGen.xml
<?xml version="1.0" encoding="UTF-8"?>
<userinfo>
<name>홍길동</name>
<age>31</age>
<phone>022770918</phone>
</userinfo>
XML문서를 JavaObject로 DataBinding Sample Source
@Resource(name = "xmlBeansMarshaller")
private Unmarshaller unmarshaller;
@Test
public void testUnmarshalling()
{
FileInputStream is = null;
try
{
is = new FileInputStream("XMLBeanGen.xml");
userDoc = (UserinfoDocument) unmarshaller.unmarshal(new StreamSource(is));
userElement = userDoc.getUserinfo();
}catch(FileNotFoundException fnde)
{
fnde.getStackTrace();
fail("testUnmarshalling failed!");
}
catch(IOException ioe)
{
ioe.getStackTrace();
}
finally
{
if (is != null)
{
try
{
is.close();
}catch(IOException ioe)
{
ioe.getStackTrace();
fail("testUnmarshalling failed!");
}
}
}
}
XML Manipulation 서비스는 XML을 생성하고, 읽고, 쓰는 등과 같은 기능과 조작 기능을 제공하는 서비스이다. XML(Extensible Markup Language)은 W3C에서 다른 특수 목적의 마크업 언어를 만드는 용도에서 권장되는 다목적 마크업 언어이다.XML은 SGML의 단순화된 부분집합이지만, 수많은 종류의 데이터를 기술하는데 적용할 수 있다.XML은 주로 다른 시스템, 특히 인터넷에 연결된 시스템끼리 데이터를 쉽게 주고받을 수 있게 하여 HTML의 한계를 극복할 목적으로 만들어졌다. XML은 W3C에서 다른 특수 목적의 마크업 언어를 만드는 용도에서 권장되는 다목적 마크업 언어이다. XML은 SGML의 단순화된 부분집합이지만, 수많은 종류의 데이터를 기술하는데 적용할 수 있다.
XML 문서를 읽어 들이는 역할을 수행하는 파서는 두 가지 종류가 있다. XML 파일의 내용을 트리 구조로 한번에 읽어 들여 객체를 생성하여 처리하는 DOM(Document Object Model) 과 각각의 태그와 내용 등이 인식될 때마다 XML 문서를 읽어 들이는 SAX(Simple API for XML)라는 기술이다.
XML 문서는 요소(element),속성(attribute),Text 등으로 구성된 트리 구조의 계층적인 정보이다. ⇒DOM을 이용하면 XML 문서의 각 요소들에 대하여 트리 구조의 객체를 읽어 들인다. DOM은 XML 문서를 나타내는 각각의 객체들에 대한 표준 인터페이스이다. DOM 파서는 XML 문서로부터 DOM 구조를 생성하는 역할을 한다.

DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
DocumentBuilder parser = facory.newDocumentBuilder();
Document dc = parser.parse("XML파일명");
Element root = xmldoc.getDocumentElemnt();
logger.debug(root);
Element root = doc.getDocumentElement();
for (Node ch = root.getFirstChild(); ch != null; ch = ch.getNextSibling() {
logger.debug(ch.getNodeName());
}
Element root = doc.getDocumentElement();
for (Node ch = root.getFirstChild(); ch != null; ch = ch.getNextSibiling()) {
if (ch.getNodeType() == Node.ELEMENT_NODE)
logger.debug(ch.getNodeName());
}
Element root = xmldoc.getDocumentElement();
getNode(root)
public static void getNode(Node n) {
for (Node ch = n.getFirstChild(); ch != null; ch = ch.getNextSibling()) {
if (ch.getNodeType() == Node.ELEMENT_NODE) {
logger.debug(ch.getNodeName());
getNode(ch);
}
}
}
public static void getNode(Node n) {
for (Node ch = n.getFirstChild(); ch != null; ch = ch.getNextSibling()) {
if (ch.getNodeType() == Node.ELEMENT_NODE) {
logger.debug(ch.getNodeName());
getNode(ch);
}
// 텍스트를 처리한다.
else if (ch.getNodeType() == Node.TEXT_NODE && ch.getNodeValue().trim().length() != 0) {
logger.debug(ch.getNodeValue());
}
}
}
private String xmlString = "";
public String printString(Node node) {
int type = node.getNodeType();
switch (type) {
case Node.DOCUMENT_NODE:
printString(((Document) node).getDocumentElement());
break;
case Node.ELEMENT_NODE:
xmlString += "<" + node.getNodeName();
NamedNodeMap attrs = node.getAttributes();
for (int i = 0; i < attrs.getLength(); i++) {
Node attr = attrs.item(i);
xmlString += " " + attr.getNodeName() + "=" + attr.getNodeValue() + "'";
xmlString += ">";
NodeList children = node.getChildNodes();
if (children != null) {
for (int i = 0; i < children.getLength(); i++) {
logger.debug(children.item(i));
}
}
break;
}
case Node.CDATA_SECTION_NODE:
xmlString += "<![CDATA[" + node.getNodeValue() + "]]>";
break;
case Node.TEXT_NODE:
xmlString += node.getNodeValue().trim();
break;
case Node.PROCESSING_INSTRUCTION_NODE:
xmlString += "<?" + node.getNodeName() + " " + node.getNodeValue() + "?>";
break;
}
if (type == Node.ELEMENT_NODE) {
xmlString += "</" + node.getNodeName() + ">";
}
return xmlString;
}
| NODE TYPE | 설 명 |
|---|---|
| Node.DOCUMENT_NODE | DocumentElement 객체 정보를 가지고 printString()을 호출 |
| Node.ELEMENT_NODE | 요소명,속성정보(NAME,VALUE)을 추출하여 xmlString에 저장하고 자손 요소 정보를 가지고 pringString()을 호출 |
| Node.CDATA_SECTION_NODE | 추출된 VALUE에 <!CDATA[와]]>을 추가 |
| Node.TEXT_NODE | VALUE만 추출 |
| Node.PROCESSING_INSTRUCTION_NODE | 추출된 값에 <? 와 ?>을 추가 |
XML 문서를 읽어 들이는 응용 프로그램 API 로서 XML 문서를 하나의 긴 문자열로 간주한다. SAX는 문자열을 앞에서 부터 차례로 읽어 가면서 요소,속성이 인식될 때 마다 EVENT를 발생시킨다.
각각의 EVENT가 발생 될 떄 마다 수행하고자 하는 기능을 이벤트 핸들러 기술을 이용하여 구현한다.


org.xml.sax.ContentHandlder
org.xml.sax.DTDHandler
org.xml.sax.EntityResolver
org.xml.sax.ErrorHandler
void characters(char[] ch,int start,int length)
void endDocument() // 문서의 끝이 인식되면 호출된다.
void endElement(String namespaceURI,String localName,String qName)
// 요소의 종료가 인식되면 호출된다.
void endPrefixMapping(String prefix) // prefix-URI 이름 공간의 종료가 인식되면 호출된다.
void ignorableWhitspace(char[] ch,int start, int length)
// 요소 내용에서 무시 가능한 공백을 인식되면 호출된다.
void processingInstruction(String target,String data) // PI가 인식되면 호출된다.
void setDocumentLocator(Locator locator) // 이벤트가 발생된 위치 정보를 알려 주는 객체 전달 시 호출된다.
void skippedEntity(Strig name) // 스킵된 엔티티가 인식되면 호출된다.
void startDocument() // 문서의 시작이 인식되면 호출된다.
void startElement(String namespaceURI,String localName,String qName,Attributes atts)
// 요소의 시작이 인식되면 호출된다.
void startPrefixMapping(String prefix,String uri)
// "prefix-URI 이름 공간의 시작이 인식되면 호출된다." 가 인식되면 호출된다.
void notationDecl(String name,String publicId,String systemId) // DTD 선언 이벤트가 인식되면 호출된다.
void unparsedEntityDecl(String name,String publicId,String systemId,String notationName)
// 파싱되지 않는 엔티티 선언 이벤트가 인식되면 호출된다.
InputSource resolveEntity(String publicId,String systemId) // 확장 엔티티 처리와 관련된 기능을 처리한다.
void error(SAXParseException exception) // 복구 가능한 오류 발생시 호출된다.
void fatalError(SAXParseException exception) // 복구 불가능한 오류 발생시 호출된다.
void warning(SAXParseException exception) // 경고 오류 발생시 오출된다.

//DefaultHandler를 상속하여 구현한 핸들러 클래스
class SampleHandler extends DefaultHandler {
public void startDocument() {
logger.debug("XML이 시작되었습니다.");
}
public void endDocument() {
logger.debug("XML이 종료되었습니다.");
}
}
SAXParserFactory spf = SAXParserFactory.newInstance();
SAXParser sp = spf.newSAXParser();
void parse(File f,DefaultHandler dh)
void parse(InputStream is, DefaultHandler dh)
void parse(InputStream is, DefaultHandler dh)
void parse(String uri, DefaultHandler dh)
// 핸들러를 등록하면서 XML 문서를 읽는 예
SampleHandler sh = new SampleHandler();
// 문서를 읽어들린다.
sp.parse(new FileInputStream("text.xml"),sh);
<bean id="xmlCofig" class="egovframework.rte.fdl.xml.EgovXmlset">
<property name="domconcrete" ref="domconcreteCont" />
<property name="saxconcrete" ref="saxconcreteCont" />
</bean>
<bean id="domconcreteCont" class="egovframework.rte.fdl.xml.EgovConcreteDOMFactory"/>
<bean id="saxconcreteCont" class="egovframework.rte.fdl.xml.EgovConcreteSAXFactory"/>
| PROPERTIES | 설 명 |
|---|---|
| domconcrete | EgovDOMValidatorService 생성하는 Concrete Class |
| saxconcrete | EgovSAXValidatorService 생성하는 Concrete Class |
<context:property-placeholder location="classpath*:spring/egovxml.properties" />
<bean id="xmlconfig" class="egovframework.rte.fdl.xml.XmlConfig">
<property name="xmlpath" value="${egovxmlsaved.path}" />
</bean>
| PROPERTIES | 설 명 |
|---|---|
| xmlpath | XML문서 생성 Directory 위치 지정 |
// XML 기본저장 디렉토리
egovxmlsaved.path=C:\\Temp\\
egovxml.properties 내용 예시
/** abstractXMLFactoryService 상속한 Class **/
@Resource(name = "domconcreteCont")
EgovConcreteDOMFactory domconcrete = null;
/** abstractXMLFactoryService 상속한 Class **/
@Resource(name = "saxconcreteCont")
EgovConcreteSAXFactory saxconcrete = null;
/** AbstractXMLUtility 상속한 DOMValidator **/
EgovDOMValidatorService domValidator = null;
/** AbstractXMLUtility 상속한 SAXValidator **/
EgovSAXValidatorService saxValidator = null;
@Test
public void ModuleTest() throws UnsupportedException {
domValidator = domconcrete.CreateDOMValidator();
logger.debug("fileName :" + fileName);
domValidator.setXMLFile(fileName);
}
@Test
public void ModuleTest() throws UnsupportedException {
saxValidator = saxconcrete.CreateSAXValidator();
logger.debug("fileName :" + fileName);
saxValidator.setXMLFile(fileName);
}
XML문서의 well-formed 검사를 하면서 Validation검사도 동시에 실행(선택)
public void WellformedValidate(boolean used, boolean isvalid, AbstractXMLUtility service) throws ValidatorException {
if (used) {
if (service.parse(isvalid)) {
if (isvalid)
logger.debug("Validation 문서입니다.");
else
logger.debug("well-formed 문서입니다.");
}
}
}
| PARAMETER | 설 명 |
|---|---|
| isvalid | Validation 검사여부 |
검색하고자하는 Element나 Attribute등을 검색식(표현식)을 통해 조회
public void XPathResult(boolean used, AbstractXMLUtility service, Document doc) throws JDOMException {
if (used) {
List list = service.getResult(doc, "//*[@*]");
viewEelement(list);
}
}
| PARAMETER | 설 명 |
|---|---|
| expr | Validation 검색식 |
| doc | Document 객체 |
입력받은 Element를 사용하여 XML문서를 생성
public void createNewXML(boolean used, AbstractXMLUtility service, Document doc, String EleName, List list, String path)
throws JDOMException, TransformerException, FileNotFoundException {
if (used)
service.createNewXML(doc, EleName, list, path);
}
| PARAMETER | 설 명 |
|---|---|
| doc | Document 객체 |
| EleName | Root 명 |
| list | 생성 Element List |
| path | 생성될 XML문서 경로 |
입력받은 Element를 XML문서에 추가
public void addElement(boolean used, AbstractXMLUtility service, Document doc, String EleName, List list, String path)
throws JDOMException, TransformerException, FileNotFoundException {
if (used)
service.addElement(doc, EleName, list, path);
}
| PARAMETER | 설 명 |
|---|---|
| doc | Document 객체 |
| EleName | Root 명 |
| list | 생성 Element List |
| path | 생성될 XML문서 경로 |
입력받은 Text Element를 XML문서에 추가
public void addTextElement(boolean used, AbstractXMLUtility service, Document doc,
String elemName, List list, String path)
throws JDOMException, TransformerException, FileNotFoundException {
if (used)
service.addTextElement(doc, elemName, list, path);
}
| PARAMETER | 설 명 |
|---|---|
| doc | Document 객체 |
| EleName | Root 명 |
| list | 생성 Element List |
| path | 생성될 XML문서 경로 |
입력받은 Text Element로 수정
public void updTextElement(boolean used, AbstractXMLUtility service, Document doc, List list, String path)
throws JDOMException, TransformerException, FileNotFoundException {
if (used)
service.updTextElement(doc, list, path);
}
| PARAMETER | 설 명 |
|---|---|
| doc | Document 객체 |
| list | 생성 Element List |
| path | 생성될 XML문서 경로 |
입력받은 Element을 삭제
public void delElement(boolean used, AbstractXMLUtility service, Document doc, String EleName, String path)
throws JDOMException, TransformerException, FileNotFoundException {
if (used)
service.delElement(doc, EleName, path);
}
| PARAMETER | 설 명 |
|---|---|
| doc | Document 객체 |
| EleName | Element 명 |
| path | 생성될 XML문서 경로 |
public void updElement(boolean used, AbstractXMLUtility service, Document doc,
String oldElement, String newElement, String path)
throws JDOMException, TransformerException, FileNotFoundException {
if (used)
service.updElement(doc, oldElement, newElement, path);
}
| PARAMETER | 설 명 |
|---|---|
| doc | Document 객체 |
| oldElement | 수정할 Element 명 |
| newElement | 수정 Element 명 |
| path | 생성될 XML문서 경로 |
객체에 대한 Pooling 기능을 제공하는 서비스이다. 객체의 생성 비용이 크고,생성 횟수가 많으면, 평균적으로 사용되는 객체의 수가 적은 경우,성능을 향상시키기 위해서 사용한다. Object Pool은 소프트웨어 디자인 패턴으로서, 객체를 필요에 따라 생성하고 파괴하는 방식이 아닌,적절한 개수의 객체를 미리 사용 가능한 상태로 생성하여 이를 이용하는 방식이다.Client는 Pool에 객체를 요청하여 객체를 얻은 후, 업무를 수행한다. 얻어온 객체를 이용하여 업무 수행을 끝마친 후, 객체를 파괴하는 것이 아니라 Pool에게 돌려주어 다른 Client가 사용할 수 있도록 한다. Object Pooling은 객체 생성 비용이 크고,객체 생성 횟수가 많으며,평균적으로 사용되는 객체의 수가 적은 경우,높은 성능의 향상을 가져다 준다.
ObjectPool은 아래와 같은 interface이다.
public interface ObjectPool {
Object borrowObject();
void returnObject(Object borrowed);
}
ObjectPool interface를 구현하여 코드를 작성하여 사용한다.
ObjectPool을 구현한 추상 클래스이다.
public abstract class BaseObjectPool<T> implements ObjectPool<T> {
public abstract T borrowObject() throws Exception;
public abstract void returnObject(T obj) throws Exception;
public abstract void invalidateObject(T obj) throws Exception;
public int getNumIdle() throws UnsupportedOperationException {
return -1;
}
public int getNumActive() throws UnsupportedOperationException {
return -1;
}
public void clear() throws Exception, UnsupportedOperationException {
throw new UnsupportedOperationException();
}
public void addObject() throws Exception, UnsupportedOperationException {
throw new UnsupportedOperationException();
}
public void close() {
closed = true;
}
protected final boolean isClosed() {
return closed;
}
protected final void assertOpen() throws IllegalStateException {
if(isClosed()) {
throw new IllegalStateException("Pool not open");
}
}
private volatile boolean closed = false;
}
| METHOD | 설 명 |
|---|---|
| borrowObject | 객체 할당 |
| returnObject | 객체 반환 |
| invalidateObject | 객체 할당 시 유효성검사 |
| getNumIdle | Idle 상태 객체 수 리턴 |
| getNumActive | Active 상태 객체 수 리턴 |
| clear | 객체 삭제 |
| addObject | Pool에 객체 추가 |
| isClosed | 객체 close여부 판단 |
| assertOpen | 객체가 사용가능한 상태판단 |
KeyedObjectPool은 이기종(異機種) 객체들로 구성된 pool 구현을 위한 interface
public interface KeyedObjectPool {
Object borrowObject(Object key);
void returnObject(Object key, Object borrowed);
}
| METHOD | 설 명 |
|---|---|
| borrowObject | 객체 할당 |
| returnObject | 객체 반환 |
org.apache.commons.pool package는 Object Pool객체 생성과 pool로부터 생성되는 object의 created와 destoryed 부분을 분리하여 구현 할 수 있도록 지원한다. PooledObjectFactory는 pooled object의 생존주기 관리를 위한 interface이다.
public interface PooledObjectFactory<T> {
PooledObject<T> makeObject() throws Exception;
void destroyObject(PooledObject<T> p) throws Exception;
boolean validateObject(PooledObject<T> p);
void activateObject(PooledObject<T> p) throws Exception;
void passivateObject(PooledObject<T> p) throws Exception;
| METHOD | 설 명 |
|---|---|
| makeObject | 객체 생성 |
| activateObject | Pool로 부터 객체를 할당 받을 때 호출된다(재초기화 할 때 이용). |
| passivateObject | Pool로 객체를 반환할 때 호출된다(초기화 할 때 이용). |
| validateObject | 객체가 유효한지 측정하기 위해 호출된다. |
| destroyObject | 객체를 삭제할 때 호출된다. |
ObjectPool 구현한 프로그램은 PoolableObjectFactory를 구현한 프로그램을 받아들이도록 구현한다면, 다양하고 독특한 ObjectPool을 구현 할 수 있다.
PooledObjectFactory를 구현한 추상 클래스이다.
public abstract class BasePooledObjectFactory<T> implements PooledObjectFactory<T> {
public abstract T create() throws Exception;
public abstract PooledObject<T> wrap(T obj);
@Override
public PooledObject<T> makeObject() throws Exception {
return wrap(create());
}
@Override
public void destroyObject(PooledObject<T> p)
throws Exception {
}
@Override
public boolean validateObject(PooledObject<T> p) {
return true;
}
@Override
public void activateObject(PooledObject<T> p) throws Exception {
}
@Override
public void passivateObject(PooledObject<T> p)
throws Exception {
}
}
KeyedObjectPools를 위한 PooledObjectFactory이다.
public interface KeyedPooledObjectFactory<K,V> {
PooledObject<V> makeObject(K key) throws Exception;
void destroyObject(K key, PooledObject<V> p) throws Exception;
boolean validateObject(K key, PooledObject<V> p);
void activateObject(K key, PooledObject<V> p) throws Exception;
void passivateObject(K key, PooledObject<V> p) throws Exception;
}
| METHOD | 설 명 |
|---|---|
| makeObject | 객체 생성 |
| activateObject | Pool로 부터 객체를 할당 받을 때 호출된다(재초기화 할 때 이용). |
| passivateObject | Pool로 객체를 반환할 때 호출된다(초기화 할 때 이용). |
| validateObject | 객체가 유효한지 측정하기 위해 호출된다. |
| destroyObject | 객체를 삭제할 때 호출된다. |
KeyedPoolableObjectFactory를 구현한 추상 클래스이다.
public abstract class BaseKeyedPooledObjectFactory<K,V>
implements KeyedPooledObjectFactory<K,V> {
public abstract V create(K key)
throws Exception;
public abstract PooledObject<V> wrap(V value);
@Override
public PooledObject<V> makeObject(K key) throws Exception {
return wrap(create(key));
}
@Override
public void destroyObject(K key, PooledObject<V> p)
throws Exception {
}
@Override
public boolean validateObject(K key, PooledObject<V> p) {
return true;
}
@Override
public void activateObject(K key, PooledObject<V> p)
throws Exception {
}
@Override
public void passivateObject(K key, PooledObject<V> p)
throws Exception {
}
}
Encryption/Decryption과 crypto 간소화를 crypto라는 목록에 넣기 위해 생성. 좌측 네비게이션 바에는 보이지만, 클릭과 url접근은 안 됨.
암호화는 시큐리티에 대처하는 가장 강력한 수단이다. 이때 본래의 메시지를 평문(Plan Text,Clear Text)이라고 부르고, 암호화된 메시지는 암호문(Cipher Text,Cryptogram)이라고 부른다. 암호화(Encryption,Ciphering)는 메시지의 내용이 불명확하도록 평문을 재구성하여 암호문을 만드는 것인데, 이 때 사용되는 메시지의 재구성 방법을 암호화 알고리즘(Encryption Algorithm)이라고 부른다. 암호화 알고리즘에서는 암호화의 비밀성을 높이기 위해 키(Key)를 사용하기도 한다. 복호화(Decyption,decipheing)란 암호화의 역과정으로, 불명확한 메시지로부터 본래의 메시지를 환원하는 과정이다. 일반적으로 복호화에도 암호화에 사용된 것과 동일한 알고리즘이 사용된다. 그리고 암호화 기법을 적용하는 암호화 및 복호화 과정으로 구성된 시스템을 암호계(Crypto System)라고 부른다. 암호계에는 키나 알고리즘이 포함되는데 하나의 비밀키(Private Key,Secret Key)를 암호화와 복호화에 모두 사용하는 관용암호계(Conventional Crypto System) 와 비밀키와 공개키를 사용하는 공개키(Public Key System)시스템으로 구분된다.
Jasypt는 오픈소스 Java library로 개발자는 암호화관련 깊은 지식이 없어도 암복화 프로그램을 개발할 수 있도록 지원한다. 여기서 설명하는 부분은 암복화 모듈로 사용한 API 중심으로 설명하겠다.
Jasypt은 binaries(byte[] objects) 암호화를 위해 org.jasypt.encryption.pbe.PBEByteEncryption interface를 구현한 org.jasypt.encryption.pbe.StandardPBEByteEncryptor를 제공한다.
PBE (Password Based Encryption) operations인 org.jasypt.encryption.pbe.StandardPBEByteEncryptor는 암호와 키를 필요하는데 알고리즘인데 설정하는 방법은 아래와 같다
암호화 하기전에 encrptor는 초기화(intialized)되어야 한다. 초기화는 아래와 같다.
Jasypt은 texts 암호화를 위해 org.jasypt.encryption.pbe.PBEStringEncryptor interface를 구현한 org.jasypt.encryption.pbe.StandardPBEStringEncryptor를 제공한다.
Jasypt는 text 암호화에 byte(binary) 암호화 방법을 사용한다.
PBE(Password Based Encryption) operations인 org.jasypt.encryption.pbe.StandardPBEStringEncryptor는 암호와 키를 필요하는데 알고리즘인데 설정하는 방법은 아래와 같다
암호화 하기전에 encrptor는 초기화(intialized)되어야 한다. 초기화는 아래와 같다.
Jasypt는 외부로 부터 공격받을 수 있는 데이타베이스 암호나 시스템 암호를 암호화할 수 있도록 지원한다.

Jasypt은 numbers 암호화를 위해 org.jasypt.encryption.pbe.PBEBingIntegerEncryptor 와 org.jasypt.encryption.pbe.PBEBingDecimalEncryptor interface를 구현한 org.jasypt.encryption.pbe.StandardPBEBigIntegerEncryptor 와 org.jasypt.encryption.pbe.StandardPBEBigDecimalEncryptor을 제공한다.
Jasypt는 number 암호화에 byte(binary) 암호화 방식을 사용한다.(Text 암호화 방식과 같다.)
PBE (Password Based Encryption) operations인 org.jasypt.encryption.pbe.StandardPBEByteEncryptor는 암호와 키를 필요하는데 알고리즘인데 설정하는 방법은 아래와 같다.
암호화 하기전에 encrptor는 초기화(intialized)되어야 한다. 초기화는 아래와 같다.
Jasypt는 암호 기반 알고리즘외에 어떠한 알고리즘도 java의 security.provider를 구현한 JCE provider를 사용한다면 사용할 수 있다.
...
Security.addProvider(new BouncyCastleProvider());
...
StandardPBEStringEncryptor mySecondEncryptor = new StandardPBEStringEncryptor();
mySecondEncryptor.setProviderName("BC"); // jce provider name
mySecondEncryptor.setAlgorithm("PBEWITHSHA256AND128BITAES-CBC-BC");
mySecondEncryptor.setPassword(myPassword);
String mySecondEncryptedText = mySecondEncryptor.encrypt(myText);
...
...
StandardStringDigester digester = new StandardStringDigester();
digester.setProvider(new BouncyCastleProvider()); // create jce provider instance
digester.setAlgorithm("WHIRLPOOL");
String digest = digester.digest(message);
...
ARIA는 경량 환경 및 하드웨어 구현을 위해 최적화된, Involutional SPN 구조를 갖는 범용 블록 암호 알고리즘이다.
ARIA는 경량 환경 및 하드웨어에서의 효율성 향상을 위해 개발되었으며, ARIA가 사용하는 대부분의 연산은 XOR과 같은 단순한 바이트 단위 연산으로 구성되어 있습니다. ARIA라는 이름은 Academy(학계),Research Institute(연구소),Agency(정부 기관)의 첫 글자들을 딴 것으로, ARIA 개발에 참여한 학.연.관의 공동 노력을 표현하고 있다.
ARIA는 지난 2004년에 국가표준기본법에 의거, 지식경제부에 의하여 국가표준(KS)으로 지정되었다.
ARIA는 블록 암호에 대한 알려진 모든 공격에 대한 내성을 갖도록 설계되었다. 일차적으로 설계자들에 의한 내부적인 안전성 분석을 거친 뒤에, 객관적인 안전성 및 효율성 평가를 위하여 NESSIE(New European Schemes for Signatures, Integrity and Encryption)의 주관 기관인 벨기에 루벤 대학으로부터 평가를 받았다. ARIA는 하드웨어 구현 및 8비트 환경에서 뛰어난 효율성을 가지고 있어 IC 카드, VPN 장비 등 다양한 환경에 적용이 가능합니다. 또한 소프트 웨어 구현에서도 벨기에 루벤 대학의 효율성 평가에서 Camellia보다 빠르고 AES에 근접하는 성능을 보였다.
| CPU | ARIA | AES | Camellia | SEED |
|---|---|---|---|---|
| Pentium III | 37.3 | 23.3 | 33.4 | 42.4 |
| Pentium IV | 49.0 | 30.5 | 83.9 | 81.3 |
이 효율성 비교표는 NESSIE의 효율성 분석 보고서와 루벤 대학의 ARIA 분석 보고서에 근거하여 작성되었다. ARIA는 128비트 키 길이의 경우 루벤 대학의 평가의 대상이었던 ver. 0.8에 비해 라운드 수가 10에서 12로 증가 하였기 때문에 사이클 수를 평가 자료의 120%로 산출하였으며, 두 보고서가 같은 기관에서 (루벤 대학의 COSIC 그룹) 수행되었으나 양쪽 자료가 정확히 같은 환경에서 수행된 것은 아니기 때문에 두 보고서의 AES, Camellia에 대한 데이터로부터 양쪽 플랫폼의 성능비를 산출하여 SEED의 속도를 추정하였다.
암복호화 서비스를 사용하기 위해서는 다음과 같이 패스워드에 대한 hash 값 기록이 필요하다.
# Message digest algorithm using EgovPasswordEncoder..
crypto.password.algorithm=SHA-256
# hashed password (ex: egovframe (SHA-256) => gdyYs/IZqY86VcWhT8emCYfqY1ahw2vtLG+/FzNqtrQ=)
crypto.hashed.password=gdyYs/IZqY86VcWhT8emCYfqY1ahw2vtLG+/FzNqtrQ=
그리고 이를 사용하기 위해 다음과 같이 property-placeholder 설정이 필요하다.
<context:property-placeholder
location="classpath*:/META-INF/spring/crypto_config.properties,classpath*:/META-INF/spring/password.properties" />
<!-- recommended location method is using file prefix.. ex) "file:/home/properties/crypto_config.properties" -->
※ 위 property 파일과 이를 사용하기 위한 property-placeholder를 사용하지 않고 아래의 xml 설정에 직접 기록하여도 된다.
<bean id="passwordEncoder" class="egovframework.rte.fdl.cryptography.EgovPasswordEncoder">
<property name="algorithm" value="${crypto.password.algorithm}" /><!-- default : SHA-256 -->
<property name="hashedPassword" value="${crypto.hashed.password}" />
</bean>
<bean id="ARIACryptoService" class="egovframework.rte.fdl.cryptography.impl.EgovARIACryptoServiceImpl">
<property name="passwordEncoder" ref="passwordEncoder" />
<property name="blockSize" value="1025" /><!-- default : 1024 -->
</bean>
<bean id="digestService" class="egovframework.rte.fdl.cryptography.impl.EgovDigestServiceImpl">
<property name="algorithm" value="SHA-256" /><!-- default : SHA-256 -->
<property name="plainDigest" value="false" /><!-- default : false -->
</bean>
<bean id="generalCryptoService" class="egovframework.rte.fdl.cryptography.impl.EgovGeneralCryptoServiceImpl">
<property name="passwordEncoder" ref="passwordEncoder" />
<property name="algorithm" value="PBEWithSHA1AndDESede" /><!-- default : PBEWithSHA1AndDESede -->
<property name="blockSize" value="1024" /><!-- default : 1024 -->
</bean>
| bean | class | 설 명 |
|---|---|---|
| passwordEncoder | EgovPasswordEncoder | Hash function 알고리즘 및 hashed된 패스워드 보관 (다른 암복호화 서비스 bean에 의해 사용됨) |
| ARIACryptoService | EgovARIACryptoServiceImpl | ARIA 알고리즘을 통한 암복호화 서비스 제공 |
| digestService | EgovDigestServiceImpl | Digest Service(Hash function) 서비스 제공 |
| generalCryptoService | EgovGeneralCryptoServiceImpl | ARIA 이외의 알고리즘(JASYPT 기반)을 통한 암복호화 서비스 제공 |
@Resource(name="ARIACryptoService")
EgovCryptoService cryptoService;
@Test
public void testString() {
String[] testString = {
"This is a testing...\nHello!",
"한글 테스트입니다...",
"!@#$%^&*()_+|~{}:\"<>?-=\\`[];',./"
};
try {
for (String str : testString) {
byte[] encrypted = cryptoService.encrypt(str.getBytes("UTF-8"), password);
byte[] decrypted = cryptoService.decrypt(encrypted, password);
assertEquals(str, new String(decrypted, "UTF-8"));
}
} catch (UnsupportedEncodingException uee) {
uee.printStackTrace();
fail();
}
}
@Resource(name="ARIACryptoService")
EgovCryptoService cryptoService;
@Test
public void testFile() {
String filePath = "/META-INF/spring/file/test.hwp";
File srcFile = new File(this.getClass().getResource(filePath).getFile());
File trgtFile;
File decryptedFile;
try {
trgtFile = File.createTempFile("tmp", "encrypted");
trgtFile.deleteOnExit();
cryptoService.encrypt(srcFile, password, trgtFile);
decryptedFile = File.createTempFile("tmp", "decrypted");
decryptedFile.deleteOnExit();
cryptoService.decrypt(trgtFile, password, decryptedFile);
assertTrue("Decrypted file not same!!",
checkFileWithHashFunction(srcFile, decryptedFile));
} catch (Exception ex) {
ex.printStackTrace();
fail(ex.getMessage());
}
}
@Resource(name="digestService")
EgovDigestService digestService;
@Test
public void testDigest() {
String data = "egovframe";
byte[] digested = digestService.digest(data.getBytes());
assertTrue(digestService.matches(data.getBytes(), digested));
}
@Resource(name="generalCryptoService")
EgovCryptoService cryptoService;
@Test
public void testString() {
String[] testString = {
"This is a testing...\nHello!",
"한글 테스트입니다...",
"!@#$%^&*()_+|~{}:\"<>?-=\\`[];',./"
};
try {
for (String str : testString) {
byte[] encrypted = cryptoService.encrypt(str.getBytes("UTF-8"), password);
byte[] decrypted = cryptoService.decrypt(encrypted, password);
assertEquals(str, new String(decrypted, "UTF-8"));
}
} catch (UnsupportedEncodingException uee) {
uee.printStackTrace();
fail();
}
}
@Resource(name="generalCryptoService")
EgovCryptoService cryptoService;
@Test
public void testBigDecimal() {
BigDecimal big = new BigDecimal(123456);
BigDecimal encrypted = cryptoService.encrypt(big, password);
BigDecimal decrypted = cryptoService.decrypt(encrypted, password);
assertEquals(big, decrypted);
}
@Resource(name="generalCryptoService")
EgovCryptoService cryptoService;
@Test
public void testFile() {
String filePath = "/META-INF/spring/file/test.hwp";
File srcFile = new File(this.getClass().getResource(filePath).getFile());
File trgtFile;
File decryptedFile;
try {
trgtFile = File.createTempFile("tmp", "encrypted");
trgtFile.deleteOnExit();
//trgtFile = new File("C:/test.enc");
//System.out.println("Temp file : " + trgtFile.toString());
cryptoService.encrypt(srcFile, password, trgtFile);
decryptedFile = File.createTempFile("tmp", "decrypted");
decryptedFile.deleteOnExit();
//decryptedFile = new File("C:/test.dec.hwp");
cryptoService.decrypt(trgtFile, password, decryptedFile);
assertTrue("Decrypted file not same!!", checkFileWithHashFunction(srcFile, decryptedFile));
} catch (IOException ioe) {
ioe.printStackTrace();
fail(ioe.getMessage());
} catch (Exception ex) {
ex.printStackTrace();
fail(ex.getMessage());
}
}
ARIA 알고리즘으로 암/복호화시 Byte[] 형태로 결과가 나오기 때문에, Base64를 통해 추가로 인코딩/디코딩 해야 한다. Base64 이용시 Apache Commons Codec의 Base64 클래스를 이용한다.
import org.apache.commons.codec.binary.Base64;
String strValue = "TEXT";
byte[] text = cryptoService.encrypt(strValue.getBytes("UTF-8"), password);
String base64enc = Base64.encodeBase64String(text);
String urlText = URLEncoder.encode(base64enc);
//Decryption
System.out.println("\n\nDecoding Test!!!");
String value = URLDecoder.decode(urlText);
byte[] base64dec = Base64.decodeBase64(value);
byte[] dectext = cryptoService.decrypt(base64dec, password);
pom.xml에 아래 내용을 추가한다.
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.9</version>
</dependency>
표준프레임워크 3.8 부터 ARIA 블록암호 알고리즘 기반 암/복호화 설정을 간소화 할 수 있는 방법을 제공한다. 내부적으로 필요한 설정을 가지고 있고, XML Schema를 통해 필요한 설정만을 추가할 수 있도록 제공한다. 또한 globals.properties 설정 파일의 중요 정보 Url, UserName, Password 항목을 암/복호화 처리 할 수 있도록 제공한다. 그외에 정보는 properties 파일에 암호화 데이터 설정후 #{egovEnvCryptoService.decrypt(’…’)} 복호화 기능을 제공한다.
설정 간소화 기능을 사용하기 위해서는 다음과 같은 xml 선언이 필요하다.
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:egov-crypto="http://maven.egovframe.go.kr/schema/egov-crypto"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://maven.egovframe.go.kr/schema/egov-crypto http://maven.egovframe.go.kr/schema/egov-crypto/egov-crypto-4.2.0.xsd">
Security에 대한 기본 설정 정보를 제공한다. algorithmKey, algorithmKeyHash 값을 하단 [Crypto algorithmKey, algorithmKeyHash 생성]에 의해 생성된 값을 입력한다. algorithmKey, algorithmKeyHash 키의 노출을 피하고 싶다면 설정 파일에서 해당 항목 삭제 후 하단 [WAS VM arguments 환경 변수 등록(옵션)]을 참고하여 진행한다.
예:
<egov-crypto:config id="egovCryptoConfig"
initial="true"
crypto="true"
algorithm="SHA-256"
algorithmKey="(생성값)"
algorithmKeyHash="(생성값)"
cryptoBlockSize="1024"
/>
| 속성 | 설명 | 필수여부 | 비고 |
|---|---|---|---|
| initial | globals.properties 연계 Url, UserName, Password 값 로드 여부(true, false) | 필수 | |
| crypto | 계정 암호화 여부(true, false) | 필수 | |
| algorithm | 계정 암호화 알고리즘 | 필수 | |
| algorithmKey | 계정 암호화키 키 | 필수 | |
| algorithmKeyHash | 계정 암호화 키 해쉬값 | 필수 | |
| cryptoBlockSize | 계정 암호화키 블록사이즈 | 필수 | |
| cryptoPropertyLocation | 설정파일 암복호화 경로 | 선택 | default=“classpath:/egovframework/egovProps/globals.properties” |
Crypto Config 설정에 algorithmKey, algorithmKeyHash 인코딩 키 생성 방법을 제공한다. 하단 코드에서 계정암호화키 키 값을 원하는 값으로 설정한다.
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.egovframe.rte.fdl.cryptography.EgovPasswordEncoder;
public class EgovEnvCryptoAlgorithmCreateTest {
private static final Logger LOGGER = LoggerFactory.getLogger(EgovEnvCryptoAlgorithmCreateTest.class);
//계정암호화키 키
public String algorithmKey = "(사용자정의 값)";
//계정암호화 알고리즘(MD5, SHA-1, SHA-256)
public String algorithm = "SHA-256";
//계정암호화키 블럭사이즈
public int algorithmBlockSize = 1024;
public static void main(String[] args) {
EgovEnvCryptoAlgorithmCreateTest cryptoTest = new EgovEnvCryptoAlgorithmCreateTest();
EgovPasswordEncoder egovPasswordEncoder = new EgovPasswordEncoder();
egovPasswordEncoder.setAlgorithm(cryptoTest.algorithm);
LOGGER.info("------------------------------------------------------");
LOGGER.info("알고리즘(algorithm) : "+cryptoTest.algorithm);
LOGGER.info("알고리즘 키(algorithmKey) : "+cryptoTest.algorithmKey);
LOGGER.info("알고리즘 키 Hash(algorithmKeyHash) : "+egovPasswordEncoder.encryptPassword(cryptoTest.algorithmKey));
LOGGER.info("알고리즘 블럭사이즈(algorithmBlockSize) :"+cryptoTest.algorithmBlockSize);
}
}
환경설정 파일에서 데이터베이스 연결 항목(Url, UserName, Password) 인코딩 키 생성 방법을 제공한다.
<!-- EgovEnvCryptoUserTest.java 설정 파일 -->
<!-- context-crypto-test.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:egov-crypto="http://maven.egovframe.go.kr/schema/egov-crypto"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://maven.egovframe.go.kr/schema/egov-crypto http://maven.egovframe.go.kr/schema/egov-crypto/egov-crypto-4.1.0.xsd">
<bean name="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource">
<property name="useCodeAsDefaultMessage">
<value>true</value>
</property>
<property name="basenames">
<list>
<value>classpath:/egovframework/egovProps/globals</value>
</list>
</property>
</bean>
<egov-crypto:config id="egovCryptoConfig"
initial="false"
crypto="true"
algorithm="SHA-256"
algorithmKey="(사용자정의 값)"
algorithmKeyHash="(생성값)"
cryptoBlockSize="1024"
/>
</beans>
// 데이터베이스 연결 항목(Url, UserName, Password) 인코딩 값 생성 JAVA
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.egovframe.rte.fdl.cryptography.EgovEnvCryptoService;
import org.egovframe.rte.fdl.cryptography.impl.EgovEnvCryptoServiceImpl;
public class EgovEnvCryptoUserTest {
private static final Logger LOGGER = LoggerFactory.getLogger(EgovEnvCryptoUserTest.class);
public static void main(String[] args) {
String[] arrCryptoString = {
"userId", //데이터베이스 접속 계정 설정
"userPassword", //데이터베이스 접속 패드워드 설정
"url", //데이터베이스 접속 주소 설정
"databaseDriver" //데이터베이스 드라이버
};
LOGGER.info("------------------------------------------------------");
ApplicationContext context = new ClassPathXmlApplicationContext(new String[]{"classpath:/context-crypto-test.xml"});
EgovEnvCryptoService cryptoService = context.getBean(EgovEnvCryptoServiceImpl.class);
LOGGER.info("------------------------------------------------------");
String label = "";
try {
for(int i=0; i < arrCryptoString.length; i++) {
if(i==0)label = "사용자 아이디";
if(i==1)label = "사용자 비밀번호";
if(i==2)label = "접속 주소";
if(i==3)label = "데이터 베이스 드라이버";
LOGGER.info(label+" 원본(orignal):" + arrCryptoString[i]);
LOGGER.info(label+" 인코딩(encrypted):" + cryptoService.encrypt(arrCryptoString[i]));
LOGGER.info("------------------------------------------------------");
}
} catch (IllegalArgumentException e) {
LOGGER.error("["+e.getClass()+"] IllegalArgumentException : " + e.getMessage());
} catch (Exception e) {
LOGGER.error("["+e.getClass()+"] Exception : " + e.getMessage());
}
}
}
......
#mysql
Globals.mysql.DriverClassName = (생성값)
Globals.mysql.Url = (생성값)
Globals.mysql.UserName = (생성값)
Globals.mysql.Password = (생성값)
......
데이터베이스 설정 파일에서 데이터베이스 연결 항목을 디코딩 하는 설정 방법을 제공한다.
<?xml version="1.0" encoding="UTF-8"?>
<beans
xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:util="http://www.springframework.org/schema/util"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-4.0.xsd">
......
<!-- MySQL -->
<beans profile="mysql">
<bean id="dataSource" class="org.apache.commons.dbcp2.BasicDataSource" destroy-method="close">
<property name="driverClassName" value="#{egovEnvCryptoService.decrypt('${Globals.mysql.DriverClassName}')}"/>
<property name="url" value="#{egovEnvCryptoService.getUrl()}" />
<property name="username" value="#{egovEnvCryptoService.getUsername()}" />
<property name="password" value="#{egovEnvCryptoService.getPassword()}" />
</bean>
</beans>
......
-Degov.crypto.algorithmKey="(사용자정의 값)" -Degov.crypto.algorithmKeyHash="(생성값)"
전자정부 표준프레임워크는 FTP 서비스제공을 위해 Apache Commons Net™ [단순 클라이언트측의 기본적인 Internet Protocol의 구현의 FTP기능을 편리하게 제공]을 오픈 소스로 채택하였다.
Apache Commons Net™은 Network utility collection 이다. Apache Commons Net™은 단순 클라이언트측의 기본적인 Internet Protocol을 구현함으로서 기본적인 프로토콜 access가 목적이기 때문에 부분적으로 object-orient 규칙에 위배가 되는 사항이 있다는 것을 참고적으로 알고 있어야 한다.
FTP란 FTP (File Transfer Protocol) 파일 전송 프로토콜로 FTP[에프 티 피]는 인터넷상의 컴퓨터들 간에 파일을 교환하기 위한 표준 프로토콜로서 가장 간단한 방법이기도 하다.
화면에 표시할 수 있는 웹 페이지와 관련 파일들을 전송하는 HTTP, 전자우편을 전송하는 SMTP 등과 같이, FTP 역시 인터넷의 TCP/IP 응용 프로토콜 중의 하나이다.
FTP는 웹 페이지 파일들을 인터넷상에서 모든 사람이 볼 수 있도록 하기 위해 저작자의 컴퓨터로부터 서버로 옮기는 과정에서 사용된다.
또한, 다른 서버들로부터 자신의 컴퓨터로 프로그램이나 파일들을 다운로드 하는 데에도 많이 사용된다.
사용자 입장에서는 간단한 명령을 이용하여 FTP를 쓰거나, 또는 그래픽 사용자 인터페이스를 제공하는 상용 프로그램을 쓸 수도 있다.
보통은 웹 브라우저도 웹 페이지로부터 선택한 프로그램을 다운로드 하는데 FTP를 사용한다.
FTP를 사용하여 서버에 있는 파일을 지우거나 이름을 바꾸거나 옮기거나 복사하는 등 갱신작업을 할 수도 있다.
FTP 서버에는 로그온을 해야하지만, 익명의 FTP를 사용하여 모든 사람들에게 공개된 파일들을 쉽게 접근할 수 있도록 하고 있다.
FTP는 보통 TCP/IP에 함께 제공되는 일련의 프로그램 속에 포함되어 있다.
Apache Commons Net™ 프로젝트(http://commons.apache.org/net/)에서 지원하는 프로토콜은 다음과 같다.
Apache Commons Net - org.apache.commons.net.ftp 의 동작흐름에 대하여 간략히 설명한다.
논리적 흐름도는 아래와 같다.
1. FTP Client를 생성
2. FTP Server에 Connect 서버에 연결한다
3. 응답이 정상적인지 확인한다.
4. FTP Server 로그인한다
5. 접속하여 여러가지 작업(list, get, put....등등)
6. FTP Server 로그아웃한다
7. FTP Server disconnect
다음은 사용예제는 FTP에 접속하여 리스트를 볼수 있는 예제이다.
private static FileInputStream inputStream;
public static void main(String[] args) {
FTPClient client = null;
// 계정 로그인
try {
client = new FTPClient();
// 한글파일명 때문에 디폴트 인코딩을 euc-kr로 한다.
client.setControlEncoding("euc-kr");
// Test 서버 정보
logger.info("Commons NET FTP Client Test Program");
logger.info("Start GO");
// Novell TEST서버에 접속
client.connect("ftp.novell.com");
logger.info("Connected to ||||||||||||||||||||||...........");
// 응답코드가 비정상일 경우 종료함
int reply = client.getReplyCode();
if (!FTPReply.isPositiveCompletion(reply)) {
client.disconnect();
logger.info("FTP server refused connection");
} else {
logger.info(client.getReplyString());
// timeout을 설정
client.setSoTimeout(10000);
// 로그인
client.login("anonymous", "anonymous");
logger.info("anonymous login success...");
// 각종 정보를 처리 (Put / Get / Append등)
client.logout();
}
} catch (Exception e) {
logger.info("해당 ftp 로그인 실패하였습니다.");
e.printStackTrace();
System.exit(-1);
} finally {
if(client != null && client.isConnected()){
try {
client.disconnect();
}catch(IOException ioe) {
ioe.printStackTrace();
}
}
}
}
FTPFile[] ftpfiles = client.listFiles("/");
if (ftpfiles != null ) {
for (int i = 0; i < ftpfiles.length; i++) {
FTPFile file = ftpfiles[i];
logger.info(file.toString()); // 파일정보
logger.info(file.getName()); // 파일명
logger.info(file.getSize()); // 파일사이즈
}
}
File get_file = new File("c:\\temp\\test.jpg");
FileOutputStream outputstream = new FileOutputStream(get_file);
boolean result = client.retrieveFile("/public/test.jpg", outputstream);
outputstream.close();
File put_file = new File("c:\\temp\\test.jpg");
inputStream = new FileInputStream(put_file);
boolean result = client.storeFile("/public/test.jpg", inputStream);
inputStream.close();
File append_file = new File("c:\\temp\\test.jpg");
inputStream = new FileInputStream(append_file);
boolean result = client.appendFile("/public/test.jpg", inputStream);
inputStream.close();
boolean result = client.rename("/public/바꾸기전.jpg", "/public/바꾼후.jpg");
boolean result = client.deleteFile("/public/삭제할.jpg");
boolean result = client.makeDirectory("/public/test");
client.sendCommand(FTPCommand.MAKE_DIRECTORY,"/public/test");
/* 파일 타입*/
client.setFileType(FTP.BINARY_FILE_TYPE);
/* 파일 전송 형태 */
client.setFileTransferMode(FTP.COMPRESSED_TRANSFER_MODE);
/* Mail - IMAP[S] Client 사용 */
public final class IMAPMail {
public static void main(String[] args) {
if (args.length < 3) {
System.err.println(
"Usage: IMAPMail <imap server hostname> <username> <password> [TLS]");
System.exit(1);
}
String server = args[0];
String username = args[1];
String password = args[2];
String proto = (args.length > 3) ? args[3] : null;
IMAPClient imap;
if (proto != null) {
System.out.println("Using secure protocol: " + proto);
imap = new IMAPSClient(proto, true); // implicit
} else {
imap = new IMAPClient();
}
System.out.println("Connecting to server " + server + " on " + imap.getDefaultPort());
imap.setDefaultTimeout(60000);
// suppress login details
imap.addProtocolCommandListener(new PrintCommandListener(System.out, true));
try {
imap.connect(server);
} catch (IOException e) {
throw new RuntimeException("Could not connect to server.", e);
}
try {
if (!imap.login(username, password)) {
System.err.println("Could not login to server. Check password.");
imap.disconnect();
System.exit(3);
}
imap.setSoTimeout(6000);
imap.capability();
imap.select("inbox");
imap.examine("inbox");
imap.status("inbox", new String[]{"MESSAGES"});
imap.logout();
imap.disconnect();
} catch (IOException e) {
System.out.println(imap.getReplyString());
e.printStackTrace();
System.exit(10);
return;
}
}
}
Version 3.0의 binary는 호환이 보장 되지만 소스코드는 아래와 같은 내용이 변경되었다.
사용되지 않는 여러 상수들이 제거되었으며, binary 호환성에 영향을 주지않는 exception, public methods 들은 더이상 IOException 을 throw 하지 않도록 보완/수정되었다.
전자정부 프레임워크에서는 이메일 발송을 쉽게 처리하기 위해 Jakarta Commons Email API를 사용하고 있는데 Commons Email은 내부적으로 Java Mail API와 JavaBeans Activation API 를 제공하여 오픈 소스로 채택하였다
Apache Commons-Email은 Java Mail API를 근간으로 좀더 심플하게 메일을 보내는 방안을 제시한다.
Commons Email API는 메일 발송을 처리해주는 SimpleEmail, HtmlEmail과 같은 클래스를 제공하고 있으며, 이들 클래스를 사용하여 일반 텍스트메일, HTML 메일, 첨부 메일 등을 매우 간단(simple)하게 발송할 수 있다.
Email Sample Code는 다음과 같다.
public class EgovSimpleMail {
public static void main(String args[])throws MailException {
SimpleEmail email = new SimpleEmail();
// setHostName에 실제 메일서버정보
email.setCharset("euc-kr"); // 한글 인코딩
email.setHostName("mail.myserver.com"); //SMTP서버 설정
try {
email.addTo("jdoe@somewhere.org", "John Doe"); // 수신자 추가
} catch (EmailException e) {
e.printStackTrace();
}
try {
email.setFrom("me@apache.org", "Me"); // 보내는 사람
} catch (EmailException e) {
e.printStackTrace();
}
email.setSubject("Test message"); // 메일 제목
email.setContent("simple 메일 Test입니다", "text/plain; charset=euc-kr");
try {
email.send();
} catch (EmailException e) {
e.printStackTrace();
}
}
}
org.apache.commons.mail.SimpleEmail 은 가장 중심이 되는 org.apache.commons.mail.Email을 상속받아 setMsg(java.lang.String msg)만을 구현한 가장 기본적인 클래스이다.
setSubject(java.lang.String subject)와 setMsg(java.lang.String msg)로 메일 제목과 내용을 입력한 후 send() 함수로 전송한다.
email.setCharset(“euc-kr”)을 사용해서 메일의 캐릭터셋이 euc-kr 이라고 지정하고 있는데, 이를 표시하지 않으면 메일 제목이나 보내는 사람 이름등에 있는 한글이 깨지게 되니 주의해야한다.
public class EgovEmailAttachment {
public static void main(String args[]) throws MailException {
try {
// 첨부할 attachment 정보를 생성합니다
EmailAttachment attachment = new EmailAttachment();
attachment.setPath("C:\\xxxx.jpg");
attachment.setDisposition(EmailAttachment.ATTACHMENT);
attachment.setDescription("첨부 관련 TEST입니다");
attachment.setName("xxxx.jpg"); //
// 기본 메일 정보를 생성합니다
MultiPartEmail email = new MultiPartEmail();
email.setCharset("euc-kr");// 한글 인코딩
email.setHostName("mail.myserver.com");
email.addTo("egov@egov.org", "전자정부");
email.setFrom("egovto@egov.org", "Me");
email.setSubject("전자 정부 첨부 파일 TEST입니다");
email.setMsg("여기는 첨부관련 내용을 입력합니다");
// 생성한 attachment를 추가합니다
email.attach(attachment);
// 메일을 전송합니다
email.send();
} catch (EmailException e) {
e.printStackTrace();
}
}
}
첨부파일을 보낼려면 org.apache.commons.mail.EmailAttachment 클래스와 org.apache.commons.mail.MultiPartEmail 이메일을 사용하면 된다.
파일 경로와 파일 설명등을 추가하여 setName(java.lang.String name)을 통해 첨부되는 파일명을 설정한다. 그후 MultiPartEmail 을 통해 SimpleEmail 처럼 기본 메일정보를 설정한다.
그리고 MultiPartEmail의 attach() 함수를 통해 첨부 파일을 추가하여 전송한다.
만약 첨부파일이 두개 이상이라면 EmailAttachment 를 여러개 생성하여 파일 정보를 설정 한 후 attach()를 통해 추가해 준다.
EmailAttachment 객체를 생성한 뒤 email.attach()를 사용해서 첨부할 파일을 추가해주기만 하면 된다. 실제 파일명은 한글이 포함되더라도, EmailAttachment.setName() 메소드를 사용해서 파일명을 변경해서 전송할 수도 있다. 주의할 점은 1.0 버전의 Commons Email은 파일명을 한글로 전달할 경우, 파일명이 올바르게 전달되지 않고 깨져서 간단하는 점이다. (파일 자체는 올바르게 전송된다.) 따라서 1.0 버전의 Common Email을 사용하여 파일을 전송할 때에는 알파벳과 숫자로만 구성된 이름의 파일을 전송한다.
public class EgovEmailAttachmentUrl {
public static void main(String args[]) throws MailException, MalformedURLException {
try {
// 첨부할 URL정보 및 파일 기본 정보를 설정합니다
EmailAttachment attachment = new EmailAttachment();
attachment.setURL(new URL("http://www.apache.org/images/asf_logo_wide.gif"));
attachment.setDisposition(EmailAttachment.ATTACHMENT);
attachment.setDescription("Apache logo");
attachment.setName("Apache logo");
// 기본 메일 정보를 생성합니다
MultiPartEmail email = new MultiPartEmail();
email.setHostName("mail.myserver.com");
email.addTo("jdoe@somewhere.org", "John Doe");
email.setFrom("me@apache.org", "Me");
email.setSubject("The logo");
email.setMsg("Here is Apache's logo");
// attachment를 추가합니다
email.attach(attachment);
// 메일을 전송합니다
email.send();
} catch (EmailException e) {
e.printStackTrace();
}
}
}
파일 경로 정보를 setURL(java.net.URL) 으로 설정할 뿐 위의 첨부파일과 동일하다.
public class EgovHtmlEmailSend {
public static void main(String args[]) throws MailException, MalformedURLException {
// 기본 메일 정보를 생성합니다
try {
HtmlEmail email = new HtmlEmail();
email.setHostName("mail.myserver.com");
email.addTo("xxxx@somewhere.org", "xxxx");
email.setFrom("me@apache.org", "Me");
email.setSubject("Test email with inline image");
// 삽입할 이미지와 그 Content Id를 설정합니다
URL url = new URL("http://www.apache.org/images/asf_logo_wide.gif");
String cid = email.embed(url, "Apache logo");
// HTML 메세지를 설정합니다
email.setHtmlMsg("<html>The apache logo - <img src=\"cid:"+cid+"\"></html>");
// HTML 이메일을 지원하지 않는 클라이언트라면 다음 메세지를 뿌려웁니다
email.setTextMsg("Your email client does not support HTML messages");
// 메일을 전송합니다
email.send();
} catch (EmailException e) {
e.printStackTrace();
}
}
}
email.setHtmlMsg()를 사용하여 메일 내용을 입력할 때 캐릭터셋을 별도로 지정하지 않아도 한글이 깨지지 않는다.
만약 SMTP 서버가 인증을 요구한다면 org.apache.commons.mail.Email 의 setAuthentication(java.lang.String username, java.lang.String password)를 통해 해결할 수 있다
이 함수는 JavaMail API의 DefaultAuthenticator 클래스를 생성하여 사용한다.
SimpleEmail email = new SimpleEmail();
email.setCharset("euc-kr");
email.setHostName("mail.somehost.com");
email.setAuthentication("madvirus", "password");
...

Apache Commons-Email UserGuide
전자정부 프레임워크에서는 다양한 압축방식을 개발자들에게 편리한 API를 제공하는 Jakarta Commons의 Compress를 오픈소스로 채택하였다.
Jakarta Commons의 Compress에서 지원하는 tar, zip and bzip2 파일등을 지원한다.
현재 Commons Compress API 에서는 아래의 Packages를 제공하고 있다.
보다 자세한 사항은 Commons Compress API를 참고하기 바란다.
압축이란 파일에 저장되어 있는 정보를 압축하여 보다 적은 기억 공간에 동일한 정보를 저장하는 기술이다.
일반적으로 정보에 포함되어 있는 중복된 내용을 삭제하거나 보다 적은 길이의 코드를 사용하여 정보를 표현하는 방법을 사용하여 저장에 필요한 공간의 크기를 줄인다.
이런 과정을 압축이라고 하며, 압축된 정보를 사용하기 위해서 다시 원래의 상태로 복원하는 과정을 압축해제라고 한다.
압축 방법에는 손실이 없는 압축 방법과 손실이 있는 압축 방법이 있다.
먼저 프로그램과 데이터 등과 같은 정보는 반드시 손실이 없는 압축 방법을 사용하여야 한다.
손실이 없는 압축 방법이라고 하는 것은 압축된 정보를 다시 복원한 경우에 압축되기 이전의 상태와 동일한 내용과 크기를 가지게 되는 압축 방법을 말한다.
하지만 이미지나 음성 등과 같은 경우에는 손실이 있는 압축 방법을 사용할 수 있다.
이미지나 음성에는 방대한 양의 정보가 존재하고 이들 정보 중에 일부가 사라진다 하더라도 사람이 눈이나 귀로 그 차이를 구별할 수 없기 때문이다.
손실이 있는 압축 방법이라고 하는 것은 압축된 정보를 다시 복원하더라도 압축되기 이전의 상태 혹은 크기와 동일하지 않은 내용을 가질 수 있는 압축 방법을 뜻한다.
간단히 여기서는 자주 사용하는 압축파일의 종류는 아래의 참조 링크를 참고한다.
다음은 테스트코드 EgovZipTestCase의 testZipArchiveCreation() 메소드로 org.apache.commons.compress.archivers 패키지에 속한 ArchiveInputStream 클래스를 사용하여 compress/decompress로 구성되어 있다.
public final class EgovZipTestCase extends EgovAbstractTestCase {
/*
* Zip을 사용한 압축(Archive) TEST
* 두개의 파일(test1.xml, test2.xml)을 사용하여 압축하여 bla.zip으로 압축함
*/
public void testZipArchiveCreation() throws Exception {
// Archive
final File output = new File(dir, "bla.zip");
final File file1 = getFile("test1.xml");
final File file2 = getFile("test2.xml");
{
final OutputStream out = new FileOutputStream(output);
final ArchiveOutputStream os = new ArchiveStreamFactory().createArchiveOutputStream("zip", out);
os.putArchiveEntry(new ZipArchiveEntry("testdata/test1.xml"));
IOUtils.copy(new FileInputStream(file1), os);
os.closeArchiveEntry();
os.putArchiveEntry(new ZipArchiveEntry("testdata/test2.xml"));
IOUtils.copy(new FileInputStream(file2), os);
os.closeArchiveEntry();
os.close();
}
// Unarchive the same
List results = new ArrayList();
{
final InputStream is = new FileInputStream(output);
final ArchiveInputStream in = new ArchiveStreamFactory().createArchiveInputStream("zip", is);
File result = File.createTempFile("dir-result", "");
result.delete();
result.mkdir();
ZipArchiveEntry entry = null;
while((entry = (ZipArchiveEntry)in.getNextEntry()) != null) {
File outfile = new File(result.getCanonicalPath() + "/result/" + entry.getName());
outfile.getParentFile().mkdirs();
OutputStream out = new FileOutputStream(outfile);
IOUtils.copy(in, out);
out.close();
results.add(outfile);
}
in.close();
}
assertEquals(results.size(), 2);
File result = (File)results.get(0);
assertEquals(file1.length(), result.length());
result = (File)results.get(1);
assertEquals(file2.length(), result.length());
}
}
다음은 위에서 압축한 파일을 압축해제(Unarchive)하는 테스트 코드이다.
public void testZipUnarchive() throws Exception {
final File input = getFile("bla.zip");
final InputStream is = new FileInputStream(input);
final ArchiveInputStream in = new ArchiveStreamFactory().createArchiveInputStream("zip", is);
final ZipArchiveEntry entry = (ZipArchiveEntry)in.getNextEntry();
final OutputStream out = new FileOutputStream(new File(dir, entry.getName()));
IOUtils.copy(in, out);
out.close();
in.close();
}
| 압축 파일 | 설명 |
|---|---|
| .alz | 이스트소프트에서 개발한 압축 형식입니다. 분할 압축을 할 경우 확장자는 (ALZip 으로 생성) alz, a00, a01…형식으로 생성됨. |
| .ace | ACE, WinAce에서 이용하는 압축 형식입니다. 분할 압축을 할 경우 확장자는 ace, c00, c01, … 형식으로 생성됨. |
| .arc | DOS용 프로그램 pkarc.com, pkxarc.com에서 사용되는 압축 형식. |
| .arj | DOS용 프로그램 arj.exe, 윈도우용 프로그램 WinArj에서 이용하는 압축 형식. 분할 압축을 할 경우 확장자는 arj, a01, a02,… 형식으로 생기게 됨. |
| .b64 | 인터넷에서 문서를 주고 받을 때 사용하는 형식으로 BASE64MIME 형식으로 인코딩된 파일임. |
| .bh | BinHex Format. E-Mail로 이진(binary) 형태의 파일을 보내기 위해 사용하는 형식임. |
| .bhx | 인터넷에서 문서를 주고 받을 때 사용하는 형식으로 BASE64MIME 형식으로 인코딩된 파일임. |
| .bin | Macintosh용이며, MacBinary Format. Aladdin StuffIt Expander에서 지원함. |
| .bz2 | UNIX용의 bzip2에서 사용하는 압축 형식입니다. 파일 하나만 압축할 수 있으므로 주로 .tar와 함께 사용되며 이 경우 .tar.bz2의 확장자를 갖음. (bzip2로 생성) |
| .cab | Microsoft Cabinet 파일. 마이크로소프트에서 사용하는 압축 형식임. |
| .ear | 내부적으로 zip 압축 알고리즘을 사용하는 파일 형식임. |
| .enc | E-Mail로 이진(binary) 형태의 파일을 보내기 위해 사용하는 형식임. |
| .gz | UNIX용의 gzip에서 사용하는 압축 형식. 파일 하나만 압축할 수 있으므로 주로 .tar와 함께 사용되며 이 경우 .tar.gz의 확장자를 갖고 줄여서 .tgz 확장자를 사용하기도 함. |
| .ha | PPMC를 개선한 압축 파일 형식. |
| .hqx | 맥에서 제작된 파일을 인터넷에서 문서를 주고 받을 때 사용하는 형식임. (BinHex로 생성) |
| .ice | DOS용 프로그램 ice.exe에서 사용하는 압축 형식임. 실제 파일 내용은 lha와 동일함. |
| .img | Disk image를 저장해둔 파일로, Falk Huth에 의해 만들어진 img.exe라는 프로그램을 이용하여 파일들을 추출해 낼 수 있음. |
| .jar | 자바의 jar.exe에서 사용하는 압축 형식. 내부적으로 zip 압축 알고리즘을 사용함. |
| .lha, .lzh | DOS용 프로그램 lha.exe, lharc.exe에서 사용하던 압축 형식. Lempel-Ziv 알고리즘을 사용함. |
| .mim | 인터넷에서 문서를 주고 받을 때 사용하는 형식. |
| .pak | DOS용 프로그램 pak.exe에서 사용하던 압축 형식. |
| .rar | DOS용 프로그램 rar.exe와 윈도우용 프로그램 winrar.exe에서 사용하는 압축 형식. 분할 압축을 할 경우 확장자는 rar, r00, r01,… 형식으로 생기게 됨. |
| .sit | Macintosh에서 이용되는 압축 Format. WinArj, Aladdin StuffIt Expander, WinPack 등의 윈도우용 압축 프로그램에서 이를 지원함. |
| .tar | UNIX 명령 tar를 이용해 생성되는 파일 형식으로 실제로는 압축은 되지 않고 여러 파일을 하나로 묶어주기만 함. 보통 tar로 묶은 후 gz으로 압축하며 이 경우 .tar.gz의 확장자를 갖고 줄여서 .tgz 확장자를 사용하기도 함. |
| .tgz | UNIX에서 tar로 묶은 파일을 gzip으로 압축한 파일 형식. |
| .uue | UU Encoded Format. E-Mail로 이진(binary) 형태의 파일을 보내기 위해 사용하는 형식. |
| .war | 내부적으로 zip 압축 알고리즘을 사용하는 파일 형식. |
| .xxe | XXEncode Format. E-Mail로 이진(binary) 형태의 파일을 보내기 위해 사용하는 형식. |
| .z | UNIX용의 compress, uncompress에서 사용하는 파일 형식. |
| .zip | 도스용 프로그램 pkzip.exe, pkunzip.exe에서 사용하는 파일 형식. |
| .zoo | DOS용 프로그램 zoo.exe에서 사용되는 파일 형식. |
| .001 | rzjoin으로 분할된 파일 형식. 압축이 아닌 단순히 001, 002…… 순서로만 분할된 파일. |
전자정부 프레임워크에서는 다양한 파일 업로드 API를 제공하는 Commons FileUpload를 오픈 소스로 채택하였다.
Spring 에서는 Commons FileUpload를 사용하여 싱글 파일 업로드에 대하여 가이드 하고 있다. 현재 Spring에서 싱글 파일 업로드에 대해서 매우 좋은 api를 제공해주고 있으나 멀티플 파일 업로드시에 동일한 이름의 여러 개의 파일을 올리려고 할 때 오류가 발생한다.
오류 사항에 대해서는 multipart multi file upload 지원 문제를 참고.
본 매뉴얼에서는 싱글 파일 업로드 보다 멀티플 파일 업로드를 가능하도록 그 대안에 대하여 설명하고자 한다.
데이터를 전송하는 방식에는 GET 방식과 POST 방식, 그리고 ENCTYPE 속성의 “multipart/form-data"이 있다.
이렇게 데이터를 전송하는데 아무 문제 없을 것 처럼 보이지만, 이 둘은 보낼 수 있는 데이터 양에 한계가 있다.
파일이나 용량이 큰 데이터를 전송할 때 문제가 생기는 것이다.
그 때 쓰는 폼 데이터 전송방식이 바로 ENCTYPE 속성의 multipart/form-data 이다.
전자정부 프레임워크에서는 스프링에서 제공하는 Apache Commons FileUpload API를 이용하여 파일 업로드를 처리하는 CommonsMultipartResolver 클래스를 제공하고 다음과 같이 설정파일에 CommonsMultipartResolver를 빈으로 등록하여 준다.
Apache Commens FileUpload 에서 싱글 파일 업로드에 대해서 매우 좋은 api를 제공해주고 있다. 하지만 멀티플 파일 업로드시 동일한 이름의 여러 개의 파일을 올리려고 할 때 오류가 발생한다.
여러 개의 파일을 올리려고 할 때 오류가 발생하는 문제에 대해서는 multipart multi file upload 지원 문제를 참고.
Spring에서 multipart를 사용한 파일 업로드에 대해서는 Spring’s multipart (fileupload) support 에서 자세하게 가이드 하였으므로 본 메뉴얼에서는 다루지 않는다.
기능 실행에 대한 이해를 돕기 위해 컨텐츠와 함께 컨텐츠에서 제시한 샘플 코드를 포함하고 있는 이클립스 프로젝트 형태의 웹 애플리케이션 샘플 프로젝트를 다운로드할 수 있다.
File Upload / Download 에 대한 설명은 아래 상세 페이지를 참고하라.
multiple files with a single file(한단 샘플)에서 사용한 JavaScript를 사용하여 추가 시 다른 form name을 추가하여 처리하는 방식을 가이드 하였으니 참고하기 바란다.
샘플 utilappSample의 Index.jsp 실행하였을 경우 브라우저에서 실행되는 화면

업로드는 한 컴퓨터 시스템에서 다른 시스템으로 파일을 전송하는 것을 말하는데, 대개 작은 컴퓨터에서 큰 컴퓨터로 옮길 때 이런 용어를 사용한다. 네트웍 사용자의 관점에서 보면, 파일을 업로드하는 것은 그 파일을 받을 수 있도록 설정된 다른 컴퓨터에 파일을 보내는 것이다. 전자게시판 상의 다른 사용자와 이미지 파일을 공유하기를 원하는 사람들은 그 전자게시판에 파일을 업로드하면 된다.
그러면 반대편 입장에 있는 사람은 그 파일을 다운로드하게 되는데, 여기서 다운로드는 대개 큰 컴퓨터에서 작은 컴퓨터로 파일을 전송하는 것을 의미한다. 인터넷 사용자의 입장에서의 다운로드란 다른 컴퓨터에서 파일을 받는 것이다.
파일 업로드 기능을 구현하기 위해서는 먼저 빈 설정 파일에 다음과 같이 MultiCommonsMultipartResolver를 정의해야한다. 본 가이드에서는 Apache Commons FileUpload에서 재공하는 CommonsMultipartResolver를 사용하기를 권장한다. CommonsMultipartResolver를 수정하여 사용할 경우 많은 부분에 시간과 노력이 들어갈 것이다.
<!-- Custom MultiFile resolver -->
<bean id="local.MultiCommonsMultipartResolver"
class="egovframework.rte.util.web.resolver.MultiCommonsMultipartResolver">
<property name="maxUploadSize" value="100000000" />
<property name="maxInMemorySize" value="100000000" />
</bean>
[권장]만약 스프링에서 제공하는 Apache Commons FileUpload API를 이용하여 파일 업로드를 처리하는 CommonsMultipartResolver를 사용하려고 하면 빈 설정파일에 다음과 같이 정의한다.
<!-- MULTIPART RESOLVERS -->
<!-- regular spring resolver -->
<bean id="spring.RegularCommonsMultipartResolver"
class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
<property name="maxUploadSize" value="100000000" />
<property name="maxInMemorySize" value="100000000" />
</bean>
또한 해당 컨트롤러의 property로 파일의 업로드 위치를 지정해주고 컨트롤러에서 setter 메소드를 통해 지정된 파일 업로드 위치를 불러올 수 있다. 사용예는 다음과 같다.
# windows NT일 경우
file.upload.path=C:\\temp
# Unix일 경우
file.upload.path=/usr/file/upload
@Resource(name = "fileUploadProperties")
Properties fileUploadProperties;
..
..
..
String uploadPath = fileUploadProperties
.getProperty("file.upload.path");
File saveFolder = new File(uploadPath);
..
..
..
파일 업로드를 위해 JSP파일의 입력 폼 타입을 file로 지정하고 form의 enctype을 multipart/form-data로 지정한다. 예시는 다음과 같다. 이때 CommonsMultipartResolver를 사용할 경우 form의 name을 다르게 설정하여야 한다. 중복된 이름을 사용할 경우 에러가 발생한다.
<form method="post" action="<c:url value='/upload/genericMulti.do'/>"
enctype="multipart/form-data">
<p>Type:
<input type="text" name="type" value="genericFileMulti" size="60" />
</p>
<p>File1:
<input type="file" name="file[]" size="60" />
</p>
<p>File2:
<input type="file" name="file[]" size="60" />
</p>
<p>
<input type="submit" value="Upload" />
</p>
</form>
다음은 파일 업로드를 위해 Controller를 구현한 모습이다.
@Controller("genericFileUploadController")
public class GenericFileUploadController {
@Resource(name = "multipartResolver")
CommonsMultipartResolver multipartResolver;
@Resource(name = "fileUploadProperties")
Properties fileUploadProperties;
@SuppressWarnings("unchecked")
@RequestMapping(value = "/upload/genericMulti.do")
public String multipartProcess(final HttpServletRequest request, Model model)
throws Exception {
final long startTime = System.nanoTime();
/*
* validate request type
*/
Assert.state(request instanceof MultipartHttpServletRequest,
"request !instanceof MultipartHttpServletRequest");
final MultipartHttpServletRequest multiRequest = (MultipartHttpServletRequest) request;
/*
* validate text input
*/
Assert.state(request.getParameter("type").equals("genericFileMulti"),
"type != genericFileMulti");
/*
* extract files
*/
final Map<String, MultipartFile> files = multiRequest.getFileMap();
Assert.notNull(files, "files is null");
Assert.state(files.size() > 0, "0 files exist");
/*
* process files
*/
String uploadPath = fileUploadProperties
.getProperty("file.upload.path");
File saveFolder = new File(uploadPath);
// 디렉토리 생성
if (!saveFolder.exists() || saveFolder.isFile()) {
saveFolder.mkdirs();
}
Iterator<Entry<String, MultipartFile>> itr = files.entrySet()
.iterator();
MultipartFile file;
List fileInfoList = new ArrayList();
String filePath;
while (itr.hasNext()) {
Entry<String, MultipartFile> entry = itr.next();
System.out.println("[" + entry.getKey() + "]");
file = entry.getValue();
if (!"".equals(file.getOriginalFilename())) {
filePath = uploadPath + "\\" + file.getOriginalFilename();
file.transferTo(new File(filePath));
FileInfoVO fileInfoVO = new FileInfoVO();
fileInfoVO.setFilePath(filePath);
fileInfoVO.setFileName(file.getOriginalFilename());
fileInfoVO.setFileSize(file.getSize());
fileInfoList.add(fileInfoVO);
}
}
// 여기서는 DB에 파일관련 정보를 저장하지 않고 단순히 success 페이지로 포워딩 하여 재확인 가능토록 함
model.addAttribute("fileInfoList", fileInfoList);
model.addAttribute("uploadPath", uploadPath);
final long estimatedTime = System.nanoTime() - startTime;
System.out.println(estimatedTime + " " + getClass().getSimpleName());
return "success";
}
}
Tomcat에서는 일반적으로 웹 어플리케이션이 GET과 POST 방식으로 파라미터를 넘겨 받을 때 request.setCharacterEncoding()을 통한 문자셋 인코딩이 필요하다.
Spring mvc 2.5.5 Multipart Multi file upload 지원부분에서 동일한 이름의 여러개의 파일을 올리려고 할 때 에러가 발생한다. 본 가이드에서는 이러한 문제가 발생하여 아직 Spring쪽에서 답변이 없는 상황이다. 이부분에 대하여 개발시 참고 하기바란다.
업로드 갯수를 고려하지 않고 동적으로 upload 폼을 추가할 경우 오류 메시지가 나온다.
org.springframework.web.multipart.MultipartException: Multiple files for field name [files] found - not supported by MultipartResolver
org.springframework.web.multipart.commons.CommonsFileUploadSupport.parseFileItems(CommonsFileUploadSupport.java:254)
org.springframework.web.multipart.commons.CommonsMultipartResolver.parseRequest(CommonsMultipartResolver.java:166)
org.springframework.web.multipart.commons.CommonsMultipartResolver.resolveMultipart(CommonsMultipartResolver.java:149)
org.springframework.web.servlet.DispatcherServlet.checkMultipart(DispatcherServlet.java:1015)
org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:851)
org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:807)
org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:571)
org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:511)
javax.servlet.http.HttpServlet.service(HttpServlet.java:637)
javax.servlet.http.HttpServlet.service(HttpServlet.java:717)
이러한 문제에 대해서 아래의 참고 자료를 참고하기 바란다.
여기서 다운로드는 대개 큰 컴퓨터에서 작은 컴퓨터로 파일을 전송하는 것을 의미한다. 인터넷 사용자의 입장에서의 다운로드란 다른 컴퓨터에서 파일을 받는 것이다.
EgovFrameWork에서는 파일 다운로드를 하기위한 DownloadController 클래스를 간단하게 구현하여 보았다.
DownloadController 클래스 예시
@Controller("downloadController")
public class DownloadController {
@Resource(name = "fileUploadProperties")
Properties fileUploadProperties;
@RequestMapping(value = "/download/downloadFile.do")
public void downloadFile(
@RequestParam(value = "requestedFile") String requestedFile,
HttpServletResponse response) throws Exception {
String uploadPath = fileUploadProperties
.getProperty("file.upload.path");
File uFile = new File(uploadPath, requestedFile);
int fSize = (int) uFile.length();
if (fSize > 0) {
BufferedInputStream in = new BufferedInputStream(
new FileInputStream(uFile));
// String mimetype = servletContext.getMimeType(requestedFile);
String mimetype = "text/html";
response.setBufferSize(fSize);d
response.setContentType(mimetype);
response.setHeader("Content-Disposition", "attachment; filename=\""
+ requestedFile + "\"");
response.setContentLength(fSize);
FileCopyUtils.copy(in, response.getOutputStream());
in.close();
response.getOutputStream().flush();
response.getOutputStream().close();
} else {
//setContentType을 프로젝트 환경에 맞추어 변경
response.setContentType("application/x-msdownload");
PrintWriter printwriter = response.getWriter();
printwriter.println("<html>");
printwriter.println("<br><br><br><h2>Could not get file name:<br>"
+ requestedFile + "</h2>");
printwriter
.println("<br><br><br><center><h3><a href='javascript: history.go(-1)'>Back</a></h3></center>");
printwriter.println("<br><br><br>© webAccess");
printwriter.println("</html>");
printwriter.flush();
printwriter.close();
}
}
}
jsp 페이지로 간단하게 구현된 예시
<%@ page contentType="text/html; charset=utf-8" pageEncoding="utf-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>Success</title>
</head>
<body>
<h1>Success</h1>
<p>All good</p>
<c:forEach var="file" items="${fileInfoList}" varStatus="status">
순번 : <c:out value="${status.count}" />
<br />
uploadedFilePath : <c:out value="${file.filePath}" />
<br />
파일명 : <a
href="#" onclick="window.open(encodeURI('<c:url value='/download/downloadFile.do?'/>requestedFile=${file.fileName}'))"><c:out
value="${file.fileName}" /></a>
<br />
파일사이즈 : <c:out value="${file.fileSize}" />
<br />
<p />
</c:forEach>
</body>
</html>
Tomcat에서는 일반적으로 웹 어플리케이션이 GET과 POST 방식으로 파라미터를 넘겨 받을 때 request.setCharacterEncoding()을 통한 문자셋 인코딩이 필요하다.
파일을 다운로드시 한글이 깨지는 문제가 발생할 경우 제우스나 WebLogic의 경우는 JSP 페이지에 아래와 같이 넣어주면 한글 깨지는 문제가 해결된다.
<%@ page contentType="text/html; charset=utf-8" pageEncoding="utf-8"%>
Tomcat에서는 한글이 깨지는 문제가 발생하는데 아래의 링크를 참조
Tomcat에서 문자셋 인코딩을 하여 한글이 깨지는 문제를 해결할 수 있다.
일반적으로 웹 어플리케이션이 GET과 POST 방식으로 파라미터를 넘겨 받을 때 request.setCharacterEncoding()을 통한 문자셋 인코딩이 필요하다.
단순히 JSP 혹은 서블릿의 최 상단에 request.setCharacterEncoding("euc-kr");을 하면 된다.
GET과 POST 방식에 상관없이 인코딩을 해준다.
POST 방식은 request.setCharacterEncoding("euc-kr");로 계속 하면된다.
하지만 GET 방식은 server.xml의 <Connector> 설정 부분을 바꿔줘야만 한다.
<Connector port="8080"
maxThreads="150" minSpareThreads="25" maxSpareThreads="75"
enableLookups="false" redirectPort="8443" acceptCount="100"
debug="0" connectionTimeout="20000"
disableUploadTimeout="true" URIEncoding="euc-kr"/>
위에서 URIEncoding="euc-kr" 부분이다.
결론적으로 Tomcat 4.x와 Tomcat 5.x 는 모두 request.setCharacterEncoding()이 필요하다는 사실에는 변함이 없다.
JSP페이지에서 링크를 생성할 때, 한글이 됐든 공백이나 특수문자를 가진 영어가 됐든, 순수하게 영어와 숫자, 밑줄 등으로만 이뤄진게 아닌 모든 파라미터를 넘길 때는 무조건 URLEncoding을 해야한다고 봐도 된다.
Web Container에 따라 URLEncoding을 안하고 넘겨도 작동하는 경우가 있는데, 동일한 웹 컨테이너라도 버전에 따라 한글을 제대로 인식하지 못하는 경우도 있고, 또 다른 컨테이너에서는 URLEncoding이 안된 한글을 전혀 인식하지 못할 수도 있다.
그러므로 무조건 표준을 따라서 java.net.URLEncoder.encode()메 소드를 사용해 인코딩해서 넘기도록 한다. 디코드 작업은 request.setCharacterEncoding()에 의해서 자동으로 이뤄지므로 해줄것이 없다. (Tomcat 3.x대- JSP Spec 1.1 -에서는 request.setCharacterEncoding()이 없으므로 String.getBytes()를 이용해 직접 디코딩을 해줘야만 했다)
위와 같이 test.jspf를 static include 할 경우에 test.jspf에 있는 한글이 모두 깨질 수 있다. test.jspf에도 한글 설정이 필요한데, 이 경우에는 test.jspf의 최 상단에 다음을 추가하면 된다.
<%@page pageEncoding="euc-kr"%>
File Handling 서비스를 적용해서 Excel 다운로드 하기 위한 Excel 정보를 설정한다.
Excel 서비스에 적용되어 있다.
FileObject writtenFile = manager.resolveFile(baseDir, this.propertyPath);
FileContent writtenContents = writtenFile.getContent();
InputStream is = writtenContents.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(is));
StringBuffer sb = new StringBuffer();
for (String line = ""; (line = reader.readLine()) != null; sb.append(line));
is.close();
Excel 파일 포맷을 다룰 수 있는 자바 라이브러리를 제공하여, 사용자들이 데이터를 Excel 파일 포맷으로 다운받거나, 대량의 Excel 데이터를 시스템에 올릴 수 있도록 지원하기 위한 서비스이다. Excel 서비스는 Apache POI 오픈소스를 사용하여 구현하였으며 주요 Excel 접근 기능 외에 Excel 다운로드, Excel 파일 업로드 등의 기능이 있다. Excel 서비스 3.0 버전에서는 기존 버전을 refactoring 하였다. 기존의 메소드(xls, xlsx)를 지원하는 메소드들의 이름을 하나로 하여 Parameter 방식으로 구분자를 추가하였다. 또한, 기존의 iBatis 뿐만 아니라 MyBatis도 지원하는 클래스를 추가하였다.
엑셀 파일을 생성하여 지정된 위치에 저장하는 기능을 제공한다.
Workbook 인스턴스를 생성하여 Excel sheet를 추가 생성할 수 있다.
엑셀 버전에 따라 엑셀 97~2003버전(xls)인 HSSFWorkbook, 엑셀 2007이상(xlsx)의 XSSFWorkbook 클래스를 사용할 수 있으며, 각 클래스별 사용 법(method)는 동일하다.
String sheetName1 = "first sheet";
String sheetName2 = "second sheet";
StringBuffer sb = new StringBuffer();
// 엑셀 필요버전에 맞는 확장자를 선택하면 됨
sb.append(fileLocation).append("/").append("testWriteExcelFile.xls");
sb.append(fileLocation).append("/").append("testWriteExcelFile.xlsx");
// Workbook을 필요버전에 맞는 클래스를 선택하면 됨
Workbook wb = new HSSFWorkbook(); // xls 버전
Workbook wb = new SXSSFWorkbook(); //xlsx 버전
wb.createSheet(sheetName1);
wb.createSheet(sheetName2);
wb.createSheet();
엑셀 파일 내 셀의 내용을 변경하고 저장한다.
저장된 엑셀파일을 로드할 수 있으며 지정한 sheet에 row와 cell 객체를 생성하여 텍스트를 저장하고 수정할 수 있다.
// xls엑셀 파일 로드
Workbook wb = excelService.loadWorkbook(filename);
// xlsx엑셀 파일 로드
XSSFWorkbook wb = null;
wb = excelService.loadWorkbook(sb.toString(), wb);
log.debug("testModifyCellContents after loadWorkbook....");
Sheet sheet = wb.getSheetAt(0);
Font f2 = wb.createFont();
CellStyle cs = wb.createCellStyle();
cs = wb.createCellStyle();
cs.setFont(f2);
cs.setWrapText(true);
Row row = sheet.createRow(rownum);
row.setHeight((short) 0x349);
Cell cell = row.createCell(cellnum);
// xls 엑셀방식일 경우
cell.setCellType(Cell.CELL_TYPE_STRING);
cell.setCellValue(new HSSFRichTextString(content));
// xlsx 엑셀방식일 경우
cell.setCellType(XSSFCell.CELL_TYPE_STRING);
cell.setCellValue(new XSSFRichTextString(content));
cell.setCellStyle(cs);
sheet.setColumnWidth(20, (int) ((50 * 8) / ((double) 1 / 20)));
FileOutputStream out = new FileOutputStream(filename);
wb.write(out);
out.close();
엑셀 파일 문서의 속성(Header, Footer)을 수정한다.
Header 및 Footer 클래스로 엑셀문서의 Header와 Footer의 값과 속성을 설정할 수 있다.
// 엑셀 파일 로드
Workbook wb = excelService.loadWorkbook(filename); // xls 버전
Workbook wb = excelService.loadWorkbook(filename, new XSSFWorkbook()); // xlsx 버전
LOGGER.debug("testModifyCellContents after loadWorkbook....");
Sheet sheet = wb.createSheet("doc test sheet");
Row row = sheet.createRow(1);
Cell cell = row.createCell(1);
cell.setCellValue(new HSSFRichTextString("Header/Footer Test")); // xls 버전
cell.setCellValue(new XSSFRichTextString("Header/Footer Test")); // xlsx 버전
// Header
Header header = sheet.getHeader();
header.setCenter("Center Header");
header.setLeft("Left Header");
header.setRight(HSSFHeader.font("Stencil-Normal", "Italic") + HSSFHeader.fontSize((short) 16) + "Right Stencil-Normal Italic font and size 16"); // xls 버전
header.setRight(XSSFOddHeader.stripFields("&IRight Stencil-Normal Italic font and size 16")); // xlsx 버전
// Footer
// xls 버전
Footer footer = sheet.getFooter();
footer.setCenter(HSSFHeader.font("Fixedsys", "Normal") + HSSFHeader.fontSize((short) 12) + "- 1 -");
// xlsx 버전
Footer footer = (XSSFOddFooter) sheet.getFooter();
footer.setCenter(XSSFOddHeader.stripFields("Fixedsys"));
footer.setLeft("Left Footer");
footer.setRight("Right Footer");
// 엑셀 파일 저장
FileOutputStream out = new FileOutputStream(filename);
wb.write(out);
out.close();
엑셀 파일을 읽어 특정 셀의 값을 얻어온다.
HSSFCell 클래스의 getRichStringCellValue, getNumericCellValue, getStringCellValue 등 다양한 type의 Cell 내용을 추출할 수 있다.
Workbook wbT = excelService.loadWorkbook(filename); // xls 버전
Workbook wbT = excelService.loadWorkbook(filename, new XSSFWorkbook()); // xlsx 버전
Sheet sheetT = wbT.getSheet("cell test sheet");
for (int i = 0; i < 100; i++) {
Row row = sheet.createRow(i);
for (int j = 0; j < 5; j++) {
Cell cell = row.createCell(j);
cell.setCellValue(new HSSFRichTextString("row " + i + ", cell " + j)); // xls 버전
cell.setCellValue(new XSSFRichTextString("row " + i + ", cell " + j)); // xlsx 버전
cell.setCellStyle(cs);
}
}
특정 셀의 속성(폰트, 사이즈 등)을 수정한다.
HSSFFont, HSSFCellStyle 등의 클래스를 이용하여 셀의 폰트, 사이즈 등의 셀 속성을 수정할 수 있다.
// 엑셀 파일 로드
Workbook wb = excelService.loadWorkbook(filename); // xls 버전
Workbook wb = excelService.loadWorkbook(filename, new XSSFWorkbook()); //xlsx 버전
Sheet sheet = wb.createSheet("cell test sheet2");
sheet.setColumnWidth((short) 3, (short) 200); // column Width
CellStyle cs = wb.createCellStyle();
Font font = wb.createFont();
font.setFontHeight((short) 16);
font.setBoldweight((short) 3);
font.setFontName("fixedsys");
cs.setFont(font);
cs.setAlignment(CellStyle.ALIGN_RIGHT); // cell 정렬
cs.setWrapText( true );
for (int i = 0; i < 100; i++) {
HSSFRow row = sheet.createRow(i);
row.setHeight((short)300); // row의 height 설정
for (int j = 0; j < 5; j++) {
HSSFCell cell = row.createCell((short) j);
cell.setCellValue(new HSSFRichTextString("row " + i + ", cell " + j)); // xls 버전
cell.setCellValue(new XSSFRichTextString("row " + i + ", cell " + j)); // xlsx 버전
cell.setCellStyle( cs );
}
}
// 엑셀 파일 저장
FileOutputStream out = new FileOutputStream(filename);
wb.write(out);
out.close();
공통 템플릿을 사용하여 일관성을 유지한다. jXLS 오픈소스를 사용하여 작성된 템플릿에 지정된 값을 저장한다.
List<PersonHourVO> persons = new ArrayList<PersonHourVO>();
PersonHourVO person = new PersonHourVO();
person.setName("Yegor Kozlov");
person.setId("YK");
person.setMon(5.0);
person.setTue(8.0);
person.setWed(10.0);
person.setThu(5.0);
person.setFri(5.0);
person.setSat(7.0);
person.setSun(6.0);
persons.add(person);
PersonHourVO person1 = new PersonHourVO();
person1.setName("Gisella Bronzetti");
person1.setId("GB");
person1.setMon(4.0);
person1.setTue(3.0);
person1.setWed(1.0);
person1.setThu(3.5);
person1.setSun(4.0);
persons.add(person1);
Map<String, Object> beans = new HashMap<String, Object>();
beans.put("persons", persons);
XLSTransformer transformer = new XLSTransformer();
transformer.transformXLS(filename, beans, sbResult.toString());
<jx:forEach var="persons" items="${persons}">
${persons.name} ${persons.id} ${persons.mon} ${persons.tue} ${persons.wed} ${persons.thu} ${persons.fri} ${persons.sat} ${persons.sun} $[C4+D4+E4+F4+G4+H4+I4]
</jx:forEach>
Total Hrs: $[SUM(C4)] $[SUM(D4)] $[SUM(E4)] $[SUM(F4)] $[SUM(G4)] $[SUM(H4)] $[SUM(I4)] $[SUM(J4)]


<bean id="categoryExcelView" class="egovframework.rte.fdl.excel.download.CategoryExcelView" />
<!--
XSSF 형태의 다운로드의 경우 다음의 View를 등록하여 사용한다.
<bean id="CategoryPOIExcelView" class="egovframework.rte.fdl.excel.download.CategoryPOIExcelView" />
-->
<bean class="org.springframework.web.servlet.view.BeanNameViewResolver">
<property name="order" value="0" />
</bean>
Controller 클래스 작성 Map 사용
@RequestMapping("/sale/listExcelCategory.do")
public ModelAndView selectCategoryList() throws Exception {
List<Map> lists = new ArrayList<Map>();
Map<String, String> mapCategory = new HashMap<String, String>();
mapCategory.put("id", "0000000001");
mapCategory.put("name", "Sample Test");
mapCategory.put("description", "This is initial test data.");
mapCategory.put("useyn", "Y");
mapCategory.put("reguser", "test");
lists.add(mapCategory);
mapCategory.put("id", "0000000002");
mapCategory.put("name", "test Name");
mapCategory.put("description", "test Deso1111");
mapCategory.put("useyn", "Y");
mapCategory.put("reguser", "test");
lists.add(mapCategory);
Map<String, Object> map = new HashMap<String, Object>();
map.put("category", lists);
return new ModelAndView("categoryExcelView", "categoryMap", map);
}
VO 사용
@RequestMapping("/sale/listExcelVOCategory.do")
public ModelAndView selectCategoryVOList() throws Exception {
List<UsersVO> lists = new ArrayList<UsersVO>();
UsersVO users = new UsersVO();
//Map<String, String> mapCategory = new HashMap<String, String>();
users.setId("0000000001");
users.setName("Sample Test");
users.setDescription("This is initial test data.");
users.setUseYn("Y");
users.setRegUser("test");
lists.add(users);
users.setId("0000000002");
users.setName("test Name");
users.setDescription("test Deso1111");
users.setUseYn("Y");
users.setRegUser("test");
lists.add(users);
Map<String, Object> map = new HashMap<String, Object>();
map.put("category", lists);
return new ModelAndView("categoryExcelView", "categoryMap", map);
}
View 클래스 작성(xls 버전)
public class CategoryExcelView extends AbstractExcelView {
private static final Logger LOGGER = LoggerFactory.getLogger(CategoryExcelView.class);
@Override
protected void buildExcelDocument(Map model, HSSFWorkbook wb, HttpServletRequest req, HttpServletResponse resp) throws Exception {
HSSFCell cell = null;
LOGGER.debug("### buildExcelDocument start !!!");
HSSFSheet sheet = wb.createSheet("User List");
sheet.setDefaultColumnWidth(12);
// put text in first cell
cell = getCell(sheet, 0, 0);
setText(cell, "User List");
// set header information
setText(getCell(sheet, 2, 0), "id");
setText(getCell(sheet, 2, 1), "name");
setText(getCell(sheet, 2, 2), "description");
setText(getCell(sheet, 2, 3), "use_yn");
setText(getCell(sheet, 2, 4), "reg_user");
LOGGER.debug("### buildExcelDocument cast");
Map<String, Object> map= (Map<String, Object>) model.get("categoryMap");
List<Object> categories = (List<Object>) map.get("category");
boolean isVO = false;
if (categories.size() > 0) {
Object obj = categories.get(0);
isVO = obj instanceof UsersVO;
}
for (int i = 0; i < categories.size(); i++) {
if (isVO) { // VO
LOGGER.debug("### buildExcelDocument VO : {} started!!", i);
UsersVO category = (UsersVO) categories.get(i);
cell = getCell(sheet, 3 + i, 0);
setText(cell, category.getId());
cell = getCell(sheet, 3 + i, 1);
setText(cell, category.getName());
cell = getCell(sheet, 3 + i, 2);
setText(cell, category.getDescription());
cell = getCell(sheet, 3 + i, 3);
setText(cell, category.getUseYn());
cell = getCell(sheet, 3 + i, 4);
setText(cell, category.getRegUser());
LOGGER.debug("### buildExcelDocument VO : {} end!!", i);
} else { // Map
LOGGER.debug("### buildExcelDocument Map : {} started!!", i);
Map<String, String> category = (Map<String, String>) categories.get(i);
cell = getCell(sheet, 3 + i, 0);
setText(cell, category.get("id"));
cell = getCell(sheet, 3 + i, 1);
setText(cell, category.get("name"));
cell = getCell(sheet, 3 + i, 2);
setText(cell, category.get("description"));
cell = getCell(sheet, 3 + i, 3);
setText(cell, category.get("useyn"));
cell = getCell(sheet, 3 + i, 4);
setText(cell, category.get("reguser"));
LOGGER.debug("### buildExcelDocument Map : {} end!!", i);
}
}
}
}
View 클래스 작성(xlsx 버전)
public class CategoryPOIExcelView extends AbstractPOIExcelView {
private static final Logger LOGGER = LoggerFactory.getLogger(CategoryPOIExcelView.class);
@Override
protected void buildExcelDocument(Map model, XSSFWorkbook wb, HttpServletRequest req, HttpServletResponse resp) throws Exception {
XSSFCell cell = null;
LOGGER.debug("### buildExcelDocument start !!!");
XSSFSheet sheet = wb.createSheet("User List");
sheet.setDefaultColumnWidth(12);
// put text in first cell
cell = getCell(sheet, 0, 0);
setText(cell, "User List");
// set header information
setText(getCell(sheet, 2, 0), "id");
setText(getCell(sheet, 2, 1), "name");
setText(getCell(sheet, 2, 2), "description");
setText(getCell(sheet, 2, 3), "use_yn");
setText(getCell(sheet, 2, 4), "reg_user");
LOGGER.debug("### buildExcelDocument cast");
Map<String, Object> map= (Map<String, Object>) model.get("categoryMap");
List<Object> categories = (List<Object>) map.get("category");
boolean isVO = false;
if (categories.size() > 0) {
Object obj = categories.get(0);
isVO = obj instanceof UsersVO;
}
for (int i = 0; i < categories.size(); i++) {
if (isVO) { // VO
LOGGER.debug("### buildExcelDocument VO : {} started!!", i);
UsersVO category = (UsersVO) categories.get(i);
cell = getCell(sheet, 3 + i, 0);
setText(cell, category.getId());
cell = getCell(sheet, 3 + i, 1);
setText(cell, category.getName());
cell = getCell(sheet, 3 + i, 2);
setText(cell, category.getDescription());
cell = getCell(sheet, 3 + i, 3);
setText(cell, category.getUseYn());
cell = getCell(sheet, 3 + i, 4);
setText(cell, category.getRegUser());
LOGGER.debug("### buildExcelDocument VO : {} end!!", i);
} else { // Map
LOGGER.debug("### buildExcelDocument Map : {} started!!", i);
Map<String, String> category = (Map<String, String>) categories.get(i);
cell = getCell(sheet, 3 + i, 0);
setText(cell, category.get("id"));
cell = getCell(sheet, 3 + i, 1);
setText(cell, category.get("name"));
cell = getCell(sheet, 3 + i, 2);
setText(cell, category.get("description"));
cell = getCell(sheet, 3 + i, 3);
setText(cell, category.get("useyn"));
cell = getCell(sheet, 3 + i, 4);
setText(cell, category.get("reguser"));
LOGGER.debug("### buildExcelDocument Map : {} end!!", i);
}
}
}
}
<bean id="excelService" class="egovframework.rte.fdl.excel.impl.EgovExcelServiceImpl">
<property name="mapClass" value="egovframework.rte.fdl.excel.upload.EgovExcelTestMapping" />
<property name="sqlSessionTemplate" ref="sqlSessionTemplate" />
</bean>
<bean id="excelBigService" class="egovframework.rte.fdl.excel.impl.EgovExcelServiceImpl">
<property name="mapClass" value="egovframework.rte.fdl.excel.upload.EgovExcelTestMapping" />
<property name="mapBeanName" value="mappingBean" />
<property name="sqlMapClient" ref="sqlMapClient" />
</bean>
<bean id="mappingBean" class="egovframework.rte.fdl.excel.upload.EgovExcelBigTestMapping" />
VO 클래스 작성
public class EmpVO implements Serializable {
private BigDecimal empNo;
private String empName;
private String job;
public BigDecimal getEmpNo() {
return empNo;
}
public void setEmpNo(BigDecimal empNo) {
this.empNo = empNo;
}
public String getEmpName() {
return empName;
}
public void setEmpName(String empName) {
this.empName = empName;
}
public String getJob() {
return job;
}
public void setJob(String job) {
this.job = job;
}
}
Mapping 클래스 작성
public class EgovExcelTestMapping extends EgovExcelMapping {
private static final Logger LOGGER = LoggerFactory.getLogger(EgovExcelTestMapping.class);
@Override
public EmpVO mappingColumn(Row row) {
Cell cell0 = row.getCell(0);
Cell cell1 = row.getCell(1);
Cell cell2 = row.getCell(2);
EmpVO vo = new EmpVO();
vo.setEmpNo(new BigDecimal(cell0.getNumericCellValue()));
vo.setEmpName(EgovExcelUtil.getValue(cell1));
vo.setJob(EgovExcelUtil.getValue(cell2));
LOGGER.debug("########### vo is {}", vo.getEmpNo());
LOGGER.debug("########### vo is {}", vo.getEmpName());
LOGGER.debug("########### vo is {}", vo.getJob());
return vo;
}
}
<sqlMap namespace="EmpBatchInsert">
<typeAlias alias="empVO" type="egovframework.rte.fdl.excel.vo.EmpVO" />
<insert id="insertEmpUsingBatch" parameterClass="empVO">
<![CDATA[
insert into EMP (
EMP_NO,
EMP_NAME,
JOB
) values (
#empNo#,
#empName#,
#job#
)
]]>
</insert>
</sqlMap>
시스템을 개발할 때 필요한 문자열 데이터를 다루기 위해 다양한 기능을 사용하도록 서비스한다. 문자열을 다루는 EgovStringUtil Service와 숫자를 다루는 EgovNumericUtil Service, 날짜형식을 다루는 EgovDateUtil Service 그리고 객체 생성 등의 EgovObjectUtil Service 4가지가 있다.
String이 특정 Pattern(정규표현식)에 부합하는지 검사한다.
@Test
public void testPatternMatch() throws Exception {
// pattern match 성공
String str = "abc-def";
pattern = "*-*";
assertTrue(EgovStringUtil.isPatternMatching(str, pattern));
// pattern match 실패
str = "abc";
assertTrue(!EgovStringUtil.isPatternMatching(str, pattern));
}
다양한 타입의 데이터를 특정 String형식(Format)으로 변환한다.
@Test
public void testTypeConversion() throws Exception {
// int => string
assertEquals("1", EgovStringUtil.integer2string(1));
// long => string
assertEquals("1000000000", EgovStringUtil.long2string(1000000000));
// float => string
assertEquals("34.5", EgovStringUtil.float2string(34.5f));
// double => string
assertEquals("34.5", EgovStringUtil.double2string(34.5));
// string => int
assertEquals(1, EgovStringUtil.string2integer("1"));
assertEquals(0, EgovStringUtil.string2integer(null, 0));
// string => float
assertEquals(Float.valueOf(34.5f), Float.valueOf(EgovStringUtil.string2float("34.5")));
assertEquals(Float.valueOf(10.5f), Float.valueOf(EgovStringUtil.string2float(null, 10.5f)));
// string => double
assertEquals(Double.valueOf(34.5), Double.valueOf(EgovStringUtil.string2double("34.5")));
assertEquals(Double.valueOf(34.5), Double.valueOf(EgovStringUtil.string2double(null, 34.5)));
// string => long
assertEquals(100000000, EgovStringUtil.string2long("100000000"));
assertEquals(100000000, EgovStringUtil.string2long(null, 100000000));
}
전체 String 중 일부를 가져온다.
@Test
public void testToSubString() throws Exception {
String source = "substring test";
assertEquals("test", EgovStringUtil.toSubString(source, 10));
assertEquals("string", EgovStringUtil.toSubString(source, 3, 9));
}
전체 String 중 앞뒤에 존재하는 공백 문자(white character)를 제거한다.
@Test
public void testStringTrim() throws Exception {
String str = " substring ";
assertEquals("substring" , EgovStringUtil.trim(str));
assertEquals("substring ", EgovStringUtil.ltrim(str));
assertEquals(" substring", EgovStringUtil.rtrim(str));
}
두 String을 붙여서 하나의 String을 생성한다.
@Test
public void testConcat() throws Exception {
String str1 = "substring";
String str2 = "test";
assertEquals("substringtest", EgovStringUtil.concat(str1, str2));
}
전체 String 중 특정 String Pattern이 있는지 찾는다.
@Test
public void testFindPattern() throws Exception {
String pattern = "\\d{4}-\\d{1,2}-\\d{1,2}";
// 일치하는 pattern 을 찾는다.
Matcher matcher = Pattern.compile(pattern).matcher("2009-02-03");
assertTrue(matcher.find());
// 일치하는 pattern 을 찾는다.
matcher = Pattern.compile(pattern).matcher("abcdef2009-02-03abcdef");
assertTrue(matcher.find());
// 일치하는 pattern 을 찾지 못한다.
matcher = Pattern.compile(pattern).matcher("abcdef2009-02-A3abcdef");
assertFalse(matcher.find());
}
숫자체크, 더하기, 빼기, 곱하기, 나누기, 올림, 내림 기능
주어진 문자열이 숫자형식인지 검사한다.
@Test
public void testIsNumber() throws Exception {
assertFalse(EgovNumericUtil.isNumber("abc"));
assertFalse(EgovNumericUtil.isNumber("!@"));
assertFalse(EgovNumericUtil.isNumber("ab-123"));
assertTrue(EgovNumericUtil.isNumber("-123"));
assertTrue(EgovNumericUtil.isNumber("1234"));
}
두 문자열 값의 덧셈을 실행한다.
@Test
public void testPlus() throws Exception {
assertEquals("400", EgovNumericUtil.plus("151", "249"));
assertEquals("400.0000", EgovNumericUtil.plus("151.7531", "248.2469"));
assertEquals("400.000", EgovNumericUtil.plus("151.7531", "248.2469", 3));
assertEquals("399.9654", EgovNumericUtil.plus("151.7531", "248.2123"));
assertEquals("399.966", EgovNumericUtil.plus("151.7531", "248.2123", 3, EgovNumericUtil.ROUND_UP));
assertEquals("399.965", EgovNumericUtil.plus("151.7531", "248.2123", 3, EgovNumericUtil.ROUND_DOWN));
assertEquals("399.97", EgovNumericUtil.plus("151.7531", "248.2123", 2, EgovNumericUtil.ROUND_HALF_UP));
}
두 문자열 값의 뺄셈을 실행한다.
@Test
public void testMinus() throws Exception {
assertEquals("89", EgovNumericUtil.minus("240", "151"));
assertEquals("96.4938", EgovNumericUtil.minus("248.2469", "151.7531"));
assertEquals("96.49380", EgovNumericUtil.minus("248.2469", "151.7531", 5));
assertEquals("96.4592", EgovNumericUtil.minus("248.2123", "151.7531"));
assertEquals("96.460", EgovNumericUtil.minus("248.2123", "151.7531", 3, EgovNumericUtil.ROUND_UP));
assertEquals("96.459", EgovNumericUtil.minus("248.2123", "151.7531", 3, EgovNumericUtil.ROUND_DOWN));
assertEquals("96.46", EgovNumericUtil.minus("248.2123", "151.7531", 2, EgovNumericUtil.ROUND_HALF_UP));
}
두 문자열 값의 곱셈을 실행한다.
@Test
public void testMultiply() throws Exception {
assertEquals("180", EgovNumericUtil.multiply("15", "12"));
assertEquals("189.6135", EgovNumericUtil.multiply("15.23", "12.45"));
assertEquals("189.61350", EgovNumericUtil.multiply("15.23", "12.45", 5));
assertEquals("189.614", EgovNumericUtil.multiply("15.23", "12.45", 3, EgovNumericUtil.ROUND_UP));
assertEquals("189.613", EgovNumericUtil.multiply("15.23", "12.45", 3, EgovNumericUtil.ROUND_DOWN));
assertEquals("189.61", EgovNumericUtil.multiply("15.23", "12.45", 2, EgovNumericUtil.ROUND_HALF_UP));
}
두 문자열 값의 나눗셈을 실행한다.
@Test
public void testDivide() throws Exception {
assertEquals("1.25", EgovNumericUtil.divide("15", "12"));
Class<Exception> exceptionClass = null;
try {
assertEquals("1.2232931726907630522088353413655", EgovNumericUtil.divide("15.23", "12.45"));
} catch (Exception e) {
log.error("### Exception : " + e.toString());
exceptionClass = (Class<Exception>) e.getClass();
} finally {
assertEquals(ArithmeticException.class, exceptionClass);
}
assertEquals("1.22", EgovNumericUtil.divide("15.23", "12.45", 5));
assertEquals("1.224", EgovNumericUtil.divide("15.23", "12.45", 3, EgovNumericUtil.ROUND_UP));
assertEquals("1.223", EgovNumericUtil.divide("15.23", "12.45", 3, EgovNumericUtil.ROUND_DOWN));
assertEquals("1.22", EgovNumericUtil.divide("15.23", "12.45", 2, EgovNumericUtil.ROUND_HALF_UP));
}
주어진 값의 반올림, 올림, 내림을 실행한다.
@Test
public void testScale() throws Exception {
assertEquals("151.754", EgovNumericUtil.setScale("151.7531", 3, EgovNumericUtil.ROUND_UP));
assertEquals("151.753", EgovNumericUtil.setScale("151.7531", 3, EgovNumericUtil.ROUND_DOWN));
assertEquals("151.753", EgovNumericUtil.setScale("151.7531", 3, EgovNumericUtil.ROUND_HALF_UP));
}
날짜계산, 현재일자 조회, 요일, 날짜형식체크 기능
주어진 날짜에 해당 년,월 또는 일자를 더하여 계산된 일자를 조회한다.
@Test
public void testCalcDate() throws Exception {
assertEquals("20100114", EgovDateUtil.getCalcDateAsString ("2009", "3", "20", 300, "day"));
assertEquals("2010", EgovDateUtil.getCalcYearAsString ("2009", "3", "20", 300, "day"));
assertEquals("01", EgovDateUtil.getCalcMonthAsString("2009", "3", "20", 300, "day"));
assertEquals("14", EgovDateUtil.getCalcDayAsString ("2009", "3", "20", 300, "day"));
assertEquals(2010, EgovDateUtil.getCalcYearAsInt ("2009", "3", "20", 300, "day"));
assertEquals(1, EgovDateUtil.getCalcMonthAsInt("2009", "3", "20", 300, "day"));
assertEquals(14, EgovDateUtil.getCalcDayAsInt ("2009", "3", "20", 300, "day"));
}
시작일자와 종료일자 및 두 시간 사이의 일자/밀리초 수를 계산한다.
@Test
public void testDayCount() throws Exception {
assertEquals(90, EgovDateUtil.getDayCount("20090101", "20090401"));
assertEquals(90, EgovDateUtil.getDayCountWithFormatter("20090101", "20090401", "yyyyMMdd"));
assertEquals(182, EgovDateUtil.getDayCountWithFormatter("2008/12/01", "2009/06/01", "yyyy/MM/dd"));
}
@Test
public void testTimeCount() throws Exception {
assertEquals(86400000, EgovDateUtil.getTimeCount("20090401", "20090402"));
assertEquals(60000, EgovDateUtil.getTimeCount("20090301000000", "20090301000100"));
assertEquals(3600000, EgovDateUtil.getTimeCount("20090301000000", "20090301010000"));
}
현재 일자를 조회한다.
@Test
public void testCurrentDate() throws Exception {
assertEquals(Calendar.getInstance().get(Calendar.YEAR), EgovDateUtil.getCurrentYearAsInt());
assertEquals(Calendar.getInstance().get(Calendar.MONTH) + 1, EgovDateUtil.getCurrentMonthAsInt());
assertEquals(Calendar.getInstance().get(Calendar.DAY_OF_MONTH), EgovDateUtil.getCurrentDayAsInt());
assertEquals(Calendar.getInstance().get(Calendar.HOUR_OF_DAY), EgovDateUtil.getCurrentHourAsInt());
assertEquals(Calendar.getInstance().get(Calendar.MINUTE), EgovDateUtil.getCurrentMinuteAsInt());
}
입력 일자의 해당 요일을 조회한다.
@Test
public void testGetDayOfWeek() throws Exception {
assertEquals("일", EgovDateUtil.getDayOfWeekAsString("2009", "03", "22"));
assertEquals("월", EgovDateUtil.getDayOfWeekAsString("2009", "03", "23"));
assertEquals("화", EgovDateUtil.getDayOfWeekAsString("2009", "03", "24"));
assertEquals("수", EgovDateUtil.getDayOfWeekAsString("2009", "03", "25"));
assertEquals("목", EgovDateUtil.getDayOfWeekAsString("2009", "03", "26"));
assertEquals("금", EgovDateUtil.getDayOfWeekAsString("2009", "03", "27"));
assertEquals("토", EgovDateUtil.getDayOfWeekAsString("2009", "03", "28"));
}
두 일자 사이에 해당 요일의 수를 조회한다.
@Test
public void testGetDayOfWeek() throws Exception {
assertEquals(5, EgovDateUtil.getDayOfWeekCount("20090301", "20090331", "일요일"));
assertEquals(4, EgovDateUtil.getDayOfWeekCount("20090301", "20090331", "토요일"));
assertEquals(22, EgovDateUtil.getDayOfWeekCount("20090101", "20090531", "일"));
assertEquals(52, EgovDateUtil.getDayOfWeekCount("20090101", "20091231", "일"));
assertEquals(52, EgovDateUtil.getDayOfWeekCount("20090101", "20091231", "금"));
assertEquals(52, EgovDateUtil.getDayOfWeekCount("20090101", "20091231", "토"));
}
해당 날짜의 형식이 적합성을 조회한다.
@Test
public void testDateFormatCheck() throws Exception {
// 형식이 틀린경우 ParseException 발생
Class<Exception> exceptionClass = null;
try {
dateFormatCheck = EgovDateUtil.dateFormatCheck("20090300");
} catch (Exception e) {
exceptionClass = (Class<Exception>) e.getClass();
} finally {
assertEquals(ParseException.class, exceptionClass);
}
}
클래스명으로 객체를 생성하며 객체는 파라미터가 없는 기본 생성자 또는 파라미터가 존재하는 생성자 등 다양한 형태로 객체를 인스턴스화 할 수 있다.
파라미터가 없는 기본 생성자로 객체를 인스턴스화 한다.
@Test
public void testInstantiate() throws Exception {
String className = "java.lang.String";
Object object = EgovObjectUtil.instantiate(className);
String string = (String) object;
string = "eGovFramework";
assertEquals("Framework", string.substring(4));
}
파라미터가 존재하는 형태의 생성자로 객체를 인스턴스화 한다.
@Test
public void testInstantiateParamConstructor() throws Exception {
String className = "java.lang.StringBuffer";
String[] types = new String[]{"java.lang.String"};
Object[] values = new Object[]{"전자정부 공통서비스"};
StringBuffer sb = (StringBuffer)EgovObjectUtil.instantiate(className, types, values);
sb.append(" 및 개발프레임워크 구축 사업");
assertEquals("전자정부 공통서비스 및 개발프레임워크 구축 사업", sb.toString());
}
화면처리 서비스그룹은 업무처리 서비스와 사용자간의 인터페이스를 담당하는 서비스로 사용자 화면 구성 및 사용자 입력 정보 검증 등의 기능을 지원한다.

MVC(Model-View-Controller) 패턴은 코드를 기능(역할)에 따라 Model, View, Controller 3가지 요소로 분리한다.
MVC 패턴은 UI 코드와 비즈니스 코드를 분리함으로써 종속성을 줄이고, 재사용성을 높이고, 보다 쉬운 변경이 가능하도록 한다.
MVC 패턴이 Web Framework에만 사용되는 단어는 아니지만, 표준프레임워크에서 “MVC 서비스”란 MVC 패턴을 활용한 Web MVC Framework를 의미한다.
오픈소스 Web MVC Framework에는 Spring MVC, Struts, Webwork, JSF 등이 있으며, 각각의 장점을 가지고 사용되고 있다.
기능상에서 큰 차이는 없으나, 아래와 같은 장점을 고려 표준프레임워크에서는 Spring Web MVC를 MVC 서비스의 기반 오픈 소스로 채택하였다.
Spring MVC에 대한 설명은 아래 상세 페이지를 참고하라.
Spring Framework은 간단한 설정만으로 Struts나 Webwork같은 Web Framework을 사용할 수 있지만, 자체적으로 MVC Web Framework을 가지고 있다. Spring MVC는 기본요소인 Model, View, Controller 외에도, 아래와 같은 특성을 가지고 있다.
Spring MVC(Model-View-Controller)의 핵심 Component는 아래와 같다.
| Component | 개요 |
|---|---|
| DispatcherServlet | Spring MVC Framework의 Front Controller, 웹요청과 응답의 Life Cycle을 주관한다. |
| HandlerMapping | 웹요청시 해당 URL을 어떤 Controller가 처리할지 결정한다. |
| Controller | 비지니스 로직을 수행하고 결과 데이터를 ModelAndView에 반영한다. |
| ModelAndView | Controller가 수행 결과를 반영하는 Model 데이터 객체와 이동할 페이지 정보(또는 View객체)로 이루어져 있다. |
| ViewResolver | 어떤 View를 선택할지 결정한다. |
| View | 결과 데이터인 Model 객체를 display한다. |
이들 컴포넌트간의 관계와 흐름을 그림으로 나타내면 아래와 같다.

이 가이드문서는 Spring 2.5.6 버젼을 기준으로 작성되었다.
Spring MVC Framework의 유일한 Front Controller인 DispatcherServlet은 Spring MVC의 핵심 요소이다. DispatcherServlet은 Controller로 향하는 모든 웹요청의 진입점이며, 웹요청을 처리하며, 결과 데이터를 Client에게 응답 한다. DispatcherServlet은 Spring MVC의 웹요청 Life Cycle을 주관한다 할 수 있다.
Client의 웹요청시에 DispatcherServlet에서 이루어지는 처리 흐름은 아래와 같다. 좀더 자세한 처리 흐름을 알고 싶다면 디버깅모드로 과정을 추적해 보는 것을 권장한다.

Spring MVC Framework을 사용하기 위해서는 web.xml에 DispatcherServlet을 설정하고, DispatcherServlet이 WebApplicationContext를 생성할수 있도록 빈(Bean) 정보가 있는 파일들도 설정해주어야 한다.
<web-app>
<!-- easycompnay라는 웹어플리케이션의 웹요청을 DispatcherServlet이 처리한다.-->
<servlet>
<servlet-name>easycompany</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
</servlet>
</web-app>
servlet-name은 DispatcherServlet이 기본(default)으로 참조할 빈 설정 파일 이름의 prefix가 되는데, (servlet-name)-servlet.xml 같은 형태이다. 위 예제와 같이 web.xml을 작성했다면 DispatcherServlet은 기본으로 /WEB-INF/easycompany-servlet.xml을 찾게 된다.
빈 설정 파일을 하나 이상을 사용하거나, 파일 이름과 경로를 직접 지정해주고 싶다면 contextConfigLocation 라는 초기화 파라미터 값에 빈 설정 파일 경로를 설정해준다.
...
<servlet>
<servlet-name>easycompany</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>
/WEB-INF/config/easycompany-web.xml
</param-value>
</init-param>
</servlet>
일반적으로 빈 설정 파일은 하나의 파일만 사용되기 보다는 persistance, service, web등 layer 단위로 나뉘게 된다. 또한, 같은 persistance, service layer의 빈을 2개 이상의 DispatcherServlet이 공통으로 사용할 경우도 있다. 이럴때는 공통빈(persistance, service)설정 정보는 ApplicationContext에, web layer의 빈들은 WebApplicationContext에 저장하는 아래와 같은 방법을 추천한다. 공통빈 설정 파일은 서블릿 리스너로 등록된 org.springframework.web.context.ContextLoaderListener로 로딩해서 ApplicationContext을 만들고, web layer의 빈설정 파일은 DispatcherServlet이 로딩해서 WebApplicationContext을 만든다.
....
<!-- ApplicationContext 빈 설정 파일-->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>
<!--빈 설정 파일들간에 구분은 줄바꿈(\n),컴마(,),세미콜론(;)등으로 한다.-->
/WEB-INF/config/easycompany-service.xml,/WEB-INF/config/easycompany-dao.xml
</param-value>
</context-param>
<!-- 웹 어플리케이션이 시작되는 시점에 ApplicationContext을 로딩하며, 로딩된 빈정보는 모든 WebApplicationContext들이 참조할 수 있다.-->
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<servlet>
<servlet-name>employee</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>
/WEB-INF/config/easycompany-service.xml
</param-value>
</init-param>
</servlet>
<servlet>
<servlet-name>webservice</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>
/WEB-INF/config/easycompany-webservice.xml
</param-value>
</init-param>
</servlet>
....
이 ApplicationContext의 빈 정보는 모든 WebApplicationContext들이 참조할 수 있다. 예를 들어, DispatcherServlet은 2개 사용하지만 같은 Service, DAO를 사용하는 web.xml을 아래와 같이 작성했다면, easycompany-servlet.xml에 정의된 빈정보는 easycompany-webservice.xml가 참조할 수 없지만, easycompany-service.xml, easycompany-dao.xml에 설정된 빈 정보는 easycompany-servlet.xml, easycompany-webservice.xml 둘 다 참조한다. ApplicationContext과 WebApplicationContext과의 관계를 그림으로 나타내면 아래와 같다.

DispatcherServlet에 Client로부터 Http Request가 들어 오면 HandlerMapping은 요청처리를 담당할 Controller를 mapping한다. Spring MVC는 interface인 HandlerMapping의 구현 클래스도 가지고 있는데, 용도에 따라 여러 개의 HandlerMapping을 사용하는 것도 가능하다. 빈 정의 파일에 HandlerMapping에 대한 정의가 없다면 Spring MVC는 기본(default) HandlerMapping을 사용한다.
기본 HandlerMapping은 BeanNameUrlHandlerMapping이며, jdk1.5 이상의 실행환경일 때, Spring 3.1이후 버전이면(egov 3.0부터) RequestMappingHandlerMapping가 기본 HandlerMapping이며, Spring 3.1이전 버전이면(egov 3.0이전 버전) DefaultAnnotationHandlerMapping가 기본 HandlerMapping이다. (DefaultAnnotationHAndlerMapping은 3.1부터 deprecated되고 RequestMappingHandlerMapping으로 대체됨)
BeanNameUrlHandlerMapping, SimpleUrlHandlerMapping 등 주요 HandlerMapping 구현 클래스는 상위 추상 클래스인 AbstractHandlerMapping과 AbstractUrlHandlerMapping을 확장하기 때문에 이 추상클래스들의 프로퍼티를 사용한다. 주요 프로퍼티는 아래와 같다.
...
<bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping" p:order="2"/>
<bean class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping" p:order="3">
...
Spring MVC(3.1이후버전) 제공하는 주요 HandlerMapping 구현 클래스는 아래와 같다.
Spring 3 버전 이전에는 @MVC에서 DefaultAnnotationHandlerMapping은 URL 단위로 interceptor를 적용할 수 없기에 전자정부프레임워크에서 아래와 같은 HandlerMapping 구현 클래스를 추가했다.
그러나 Spring 3버전부터 mvc:interceptors element에서 url별로 interceptor를 적용할 수 있도록 추가하여 SimpleUrlAnnotationHandlerMapping은 deprecated되었다.
BeanNameUrlHandlerMapping은 빈정의 태그에서 name attribute에 선언된 URL과 class attribute에 정의된 Controller를 매핑하는 방식으로 동작한다. 예를 들어, 아래와 같이 정의되어 있다면,
<beans ...>
...
<!--HandlerMapping이 BeanNameUrlHandlerMapping 밖에 없다면 BeanNameUrlHandlerMapping에 대한 별도의 빈정의는 필요 없다.-->
<!--<bean class="org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping"/>-->
<bean name="/insertEmployee.do" class="com.easycompany.controller.InsertEmployeeController">
...
</bean>
...
</beans>
Client에서 URL ~~/insertEmployee.do 요청이 들어오면 InsertEmployeeController 클래스가 요청 처리를 담당한다.
앞 개요에서 언급했듯이 WAC(WebgApplicationContext)에 HandlerMapping 빈정의가 없다면 BeanNameUrlHandlerMapping이 (별도의 빈 정의 없이) 사용된다. 하지만, SimpleUrlHandlerMapping 같은 다른 HandlerMapping과 같이 써야 한다면, BeanNameUrlHandlerMapping도 빈정의가 되어야 한다.
<beans ...>
...
<bean class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
<property name="mappings">
....
</property>
</bean>
<bean class="org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping"/>
<bean name="/insertEmployee.do" class="com.easycompany.controller.InsertEmployeeController">
....
</bean>
...
</beans>
@MVC 개발을 하려면 RequestMappingHandlerMapping을(Spring 3.1이전버전은 DefaultAnnotationHandlerMapping) 사용해야 한다. 단, jdk 1.5 이상의 개발환경이어야 한다. RequestMappingHandlerMapping 사용 방법은 세가지가 있다.
<mvc:annotation-driven/>을 선언하는 방법RequestMappingHandlerMapping은 기본 HandlerMapping이므로 지정하지 않아도 사용가능하다. 아래와 같이 컴포넌트 스캔할 패키지를 지정해 주면,
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-2.5.xsd">
<context:component-scan base-package="org.mycode.controller" />
</beans>
패키지 org.mycode.controller 아래의 @Controller중에 @RequestMapping에 선언된 URL과 해당 @Controller 클래스의 메소드와 매핑한다.
@MVC사용 시 필요한 빈들을 등록해주는 <mvc:annotation-driven/>을 설정하면 내부에서
org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping ,org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter 이 구성된다.
<mvc:annotation-driven/>는 DefaultAnnotationHandlerMapping, AnnotationMethodHandlerAdapter를 구성해주었다.<mvc:annotation-driven>은 다음과 같이 사용한다.
<?xml version="1.0" encoding="UTF-8"?>
<beans ... 생략>
<mvc:annotation-driven/>
<!-- 생략 -->
</beans>
다른 HandlerMapping과 함께 사용할 때 선언한다.
<bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping"/>
<!--생략-->
위와 같이 RequestMappingHandlerMapping을 설정하였을 때
Controller에서의 간단한 예제를 보면,
package org.mycode.controller;
....
@Controller
public class HelloController {
@RequestMapping(value="/hello.do")
public String hellomethod(){
......
}
}
/hello.do로 URL 요청이 들어 오면 HelloController의 메소드 hellomethod가 실행된다.
ControllerClassNameHandlerMapping은 빈정의된 Controller의 클래스 이름중 suffix인 Controller를 제거한 나머지 이름의 소문자로 url mapping한다.
<beans ..>
...
<bean class="org.springframework.web.servlet.mvc.support.ControllerClassNameHandlerMapping"/>
<bean class="com.easycompany.controller.hierarchy.EmployeeListController"/>
<bean class="com.easycompany.controller.hierarchy.InsertEmployeeController"/>
...
</beans>
빈 정의가 위와 같다면, EmployeeListController ↔ /employeelist*, InsertEmployeeController ↔ /insertemployee* 과 같이 url mapping이 이루어 진다. ControllerClassNameHandlerMapping에 프로퍼티 값으로 caseSensitive나 pathPrefix, basePackage등을 설정할 수 있는데,
<beans ..>
...
<bean class="org.springframework.web.servlet.mvc.support.ControllerClassNameHandlerMapping">
<property name="pathPrefix" value="/easycompany"/>
<property name="caseSensitive" value="true"/>
<property name="basePackage" value="com.easycompany.controller"/>
</bean>
<bean class="com.easycompany.controller.hierarchy.EmployeeListController"/>
<bean class="com.easycompany.controller.hierarchy.InsertEmployeeController"/>
...
</beans>
하면, EmployeeListController ↔ /easycompany/hierarchy/employeeList*, InsertEmployeeController ↔ /easycompany/hierarchy/insertEmployee* 과 같이 url mapping이 이루어 진다.
SimpleUrlHandlerMapping은 Ant-Style 패턴 매칭을 지원하며, 하나의 Controller에 여러 URL을 mapping 할 수 있다. proerty의 key 값에 URL 패턴을 지정하고 value에는 Controller의 id 혹은 이름을 지정한다.
<beans ...>
...
<bean class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
<property name="mappings">
<props>
<prop key="/employeeList.do">employeeListController</prop>
<prop key="/insertEmployee.do">insertEmployeeController</prop>
<prop key="/updateEmployee.do">updateEmployeeController</prop>
<prop key="/loginProcess.do">loginController</prop>
<prop key="/**/login.do">staticPageController</prop>
<prop key="/static/*.html">staticPageController</prop>
</props>
</property>
</bean>
<bean id="loginController" class="com.easycompany.controller.hierarchy.LoginController"/>
<bean id="employeeListController" class="com.easycompany.controller.hierarchy.EmployeeListController" />
<bean id="insertEmployeeController" class="com.easycompany.controller.hierarchy.InsertEmployeeController" />
<bean id="updateEmployeeController" class="com.easycompany.controller.hierarchy.UpdateEmployeeController" />
<bean id="staticPageController" class="org.springframework.web.servlet.mvc.UrlFilenameViewController" />
...
</beans>
SimpleUrlHandlerMapping을 사용하면 Interceptor를 특정 URL 단위로 적용하는게 가능하다. 프로퍼티 interceptors에 적용하려는 Interceptor들을 리스트로 선언해주면 된다. URL /employeeList.do, /insertEmployee.do, /updateEmployee.do 요청에 대해서 사용자 인증여부를 interceptor로 검증한다고 하면,아래와 같이 정의한다.
<beans ...>
...
<bean class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
<property name="interceptors">
<list>
<ref local="authenticInterceptor"/>
</list>
</property>
<property name="mappings">
<props>
<prop key="/employeeList.do">employeeListController</prop>
<prop key="/insertEmployee.do">insertEmployeeController</prop>
<prop key="/updateEmployee.do">updateEmployeeController</prop>
</props>
</property>
</bean>
<bean class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
<property name="mappings">
<props>
<prop key="/**/login.do">staticPageController</prop>
<prop key="/static/*.html">staticPageController</prop>
</props>
</property>
</bean>
<bean id="loginController" class="com.easycompany.controller.hierarchy.LoginController"/>
<bean id="employeeListController" class="com.easycompany.controller.hierarchy.EmployeeListController" />
<bean id="insertEmployeeController" class="com.easycompany.controller.hierarchy.InsertEmployeeController" />
<bean id="updateEmployeeController" class="com.easycompany.controller.hierarchy.UpdateEmployeeController" />
<bean id="staticPageController" class="org.springframework.web.servlet.mvc.UrlFilenameViewController" />
<bean id="authenticInterceptor" class="com.easycompany.interceptor.AuthenticInterceptor" />
...
</beans>
<mvc:interceptors> element를 통해 SimpleUrlAnnotationHandlerMapping과 동일한 기능을 제공하므로 deprecated되었다.DefaultAnnotationHandlerMapping에 interceptor를 등록하면, 모든 @Controller에 interceptor가 적용되는 문제점이 있다.
SimpleUrlAnnotationHandlerMapping은 @Controller 사용시에 url 단위로 Interceptor를 적용하기 위해 개발되었다.
Spring 3부터 <mvc:interceptors>를 이용하여 동일한 기능을 제공하므로 현재 SimpleUrlAnnotationHandlerMapping은 deprecated되었다.
그러나 이전 버전은 해당기능을 제공하지 않는다.
SimpleUrlAnnotationHandlerMapping은 아래와 같은 3가지 사항이 고려됬다.
웹 어플리케이션이 초기 구동될때, DefaultAnnotationHandlerMapping은 2가지 주요한 작업을 한다. (다른 HandlerMapping도 유사한 작업을 한다.)
1번 작업은 DefaultAnnotationHandlerMapping의 상위 클래스인 AbstractDetectingUrlHandlerMapping에서 이루어 지는데, 맵핑을 위한 url리스트를 가져오는 determineUrlsForHandler 메소드는 하위 클래스에서 구현하도록 abstract 선언 되어 있다.
public abstract class AbstractDetectingUrlHandlerMapping extends AbstractUrlHandlerMapping {
...
protected void detectHandlers() throws BeansException {
if (logger.isDebugEnabled()) {
logger.debug("Looking for URL mappings in application context: " + getApplicationContext());
}
String[] beanNames = (this.detectHandlersInAncestorContexts ?
BeanFactoryUtils.beanNamesForTypeIncludingAncestors(getApplicationContext(), Object.class) :
getApplicationContext().getBeanNamesForType(Object.class));
// Take any bean name that we can determine URLs for.
for (int i = 0; i < beanNames.length; i++) {
String beanName = beanNames[i];
String[] urls = determineUrlsForHandler(beanName);
if (!ObjectUtils.isEmpty(urls)) {
// URL paths found: Let's consider it a handler.
registerHandler(urls, beanName);
}
else {
if (logger.isDebugEnabled()) {
logger.debug("Rejected bean name '" + beanNames[i] + "': no URL paths identified");
}
}
}
}
protected abstract String[] determineUrlsForHandler(String beanName);
}
DefaultAnnotationHandlerMapping의 determineUrlsForHandler 메소드는 @RequestMapping의 url 리스트를 전부 가져오기 때문에, 빈 설정 파일에 정의한 url 리스트만 가져오도록 SimpleUrlAnnotationHandlerMapping에서 determineUrlsForHandler 메소드를 구현 한다.
package egovframework.rte.ptl.mvc.handler;
...
public class SimpleUrlAnnotationHandlerMapping extends DefaultAnnotationHandlerMapping {
//url리스트, 중복값을 허용하지 않음으로 Set 객체에 담는다.
private Set<String> urls;
public void setUrls(Set<String> urls) {
this.urls = urls;
}
/**
* @RequestMapping로 선언된 url중에 프로퍼티 urls에 정의된 url만 remapping해 return
* url mapping시에는 PathMatcher를 사용하는데, 별도로 등록한 PathMatcher가 없다면 AntPathMatcher를 사용한다.
* @param urlsArray - @RequestMapping로 선언된 url list
* @return urlsArray중에 설정된 url을 필터링해서 return.
*/
private String[] remappingUrls(String[] urlsArray) {
if (urlsArray==null) {
return null;
}
ArrayList<String> remappedUrls = new ArrayList<String>();
for(Iterator<String> it = this.urls.iterator(); it.hasNext();) {
String urlPattern = (String) it.next();
for(int i=0;i<urlsArray.length;i++){
if(getPathMatcher().matchStart(urlPattern, urlsArray[i])){
remappedUrls.add(urlsArray[i]);
}
}
}
return (String[]) remappedUrls.toArray(new String[remappedUrls.size()]);
}
/**
* @RequestMapping로 선언된 url을 필터링하기 위해
* org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping의
* 메소드 protected String[] determineUrlsForHandler(String beanName)를 override.
*
* @param beanName - the name of the candidate bean
* @return 빈에 해당하는 URL list
*/
protected String[] determineUrlsForHandler(String beanName) {
return remappingUrls(super.determineUrlsForHandler(beanName));
}
}
인터셉터를 적용할 url들을 프로퍼티 urls에 선언하면 되며, Ant-style의 패턴 매칭이 지원된다. SimpleUrlAnnotationHandlerMapping은 선언된 url만을 Controller와 매핑처리한다. 따라서, 아래와 같이 선언된 DefaultAnnotationHandlerMapping와 같이 선언되어야 하며, 우선순위는 SimpleUrlAnnotationHandlerMapping이 높아야 한다.
<bean id="selectAnnotaionMapper"
class="egovframework.rte.ptl.mvc.handler.SimpleUrlAnnotationHandlerMapping"
p:order="1">
<property name="interceptors">
<list>
<ref local="authenticInterceptor"/>
</list>
</property>
<property name="urls">
<set>
<value>/*Employee.do</value>
<value>/employeeList.do</value>
</set>
</property>
</bean>
<bean id="annotationMapper"
class="org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping"
p:order="2"/>
<bean id="authenticInterceptor" class="com.easycompany.interceptor.AuthenticInterceptor" />
Spring 3부터 mvc태그를 통하여 Controller처리를 위한 설정을 쉽게 하도록 Spring mvc 네임스페이스를 제공한다.
Spring 3.0부터 제공하는 mvc 태그 설정이다. Annotation기반의 Controller호출 설정과 필요한 bean설정을 편리하게 하도록 만들어졌다. 그러나 내부 수정이 어렵기 때문에 mvc:annotation-driven에서 제공하는 기능에 대하여 잘 숙지하고 변경이 불가능 한 경우에는 mvc:annotation-driven을 쓰지 않고 필요한 bean을 수동으로 넣어줘야하는 경우도 있다. mvc:annotation-driven에서 쓰는 bean설정을 중복으로 쓰지 않도록 주의한다.
(다음 설정과 동일한 동작을 한다.)
<bean class="org.springframework.http.converter.ByteArrayHttpMessageConverter" />
<bean class="org.springframework.http.converter.StringHttpMessageConverter">
<property name="writeAcceptCharset" value="false" />
</bean>
<bean class="org.springframework.http.converter.ResourceHttpMessageConverter" />
<bean class="org.springframework.http.converter.xml.SourceHttpMessageConverter" />
<bean class="org.springframework.http.converter.xml.XmlAwareFormHttpMessageConverter" />
<!-- jaxb2라이브러리 존재시 -->
<bean class="org.springframework.http.converter.xml.Jaxb2RootElementHttpMessageConverter" />
<!-- jackson 라이브러리 존재시 -->
<bean class="org.springframework.http.converter.json.MappingJacksonHttpMessageConverter"/>
<!-- rome 라이브러리 존재시 -->
<bean class="org.springframework.http.converter.feed.AtomFeedHttpMessageConverter" />
<bean class="org.springframework.http.converter.feed.RssChannelHttpMessageConverter" />
<spring:eval>에서 ConversionService를 적용되도록 지원<mvc:annotation-driven>을 사용할 때는 직접 RequestMappingHandlerAdapter를 등록해주어서는 안되며 직접 등록이 필요한 경우에는 <mvc:annotation-driven>을 설정하지 않고 각각의 필요한 설정을 수동으로 해주어야 한다. 전자정부 프레임워크 3.0에서는 Controller의 파라미터로 CommandMap을 쓰기 위하여 RequestMappingHandlerAdapter를 상속받은 EgovRequestMappingHandlerAdapter를 만들었으며, CommandMap을 써야하는 경우 <mvc:annotation-driven>을 설정하지 않고 EgovRequestMappingHandlerAdapter를 직접 선언하도록 가이드하고 있다.<mvc:annotation-driven> 내부 설정으로 WebArgumentResolver인터페이스 구현체를 쓸 수 있으나 이는 RequestMappingHandlerAdapter가 처리하는 것이 아니라 ServletWebArgumentResolverAdapter에서 호환하도록 변경해주는 것이다.기존 HandlerMapping에는 Interceptor를 모든 url에 일괄적으로만 적용할 수 있었기 때문에 전자정부 프레임워크에서는 SimpleUrlAnnotationHandlerMapping을 제공하여 url별로 Interceptor를 걸 수 있도록 하였다. 그러나 Spring 3부터 제공하는 mvc:interceptors태그를 통해 url마다 Interceptor를 적용할 수 있도록 Spring mvc태그 스키마에서 제공하고 있다. 따라서 SimpleUrlAnnotationHandlerMapping은 deprecated되었으며 url별로 Interceptor를 적용하기 위해서는 mvc:interceptors태그를 사용하도록 한다.
Interceptor를 일괄 적용하기 위해서는 다음의 예와 같이 사용한다.
<mvc:interceptors>
<bean class="egov.interceptors.EgovInterceptor"/>
</mvc:interceptors>
특정 패턴의 url에만 인터셉터를 적용하기 위해서는 <mvc:interceptors>태그 내부에 <mvc:interceptor>를 사용한다.
만약 /egov/sample로 시작되는 URL요청에만 인터셉터를 정의하기 위해서는 다음과 같이 사용할 수 있다.
<mvc:interceptors>
<mvc:interceptor>
<mvc:mapping path="/egov/sample/*"/>
<bean class="egov.interceptors.EgovInterceptor"/>
</mvc:interceptor>
</mvc:interceptors>
또한 interceptor에 특정 URL Pattern을 제외하여 맵핑하는 기능도 지원하고 있다. 이 때는 <mvc:interceptor>내부에서 <exclude-mapping>태그를 사용한다.
만약 /egov/로 시작하는 URL중 /egov/admin/으로 시작하는 URL에 interceptor맵핑을 제외하고 싶으면 다음과 같이 사용한다.
<exclude-mapping> 태그는 spring 3.2 버전 부터 사용 가능 합니다.
<mvc:interceptors>
<mvc:interceptor>
<mvc:mapping path="/egov/**"/>
<mvc:exclude-mapping path="/egov/admin/**"/>
<bean class="egov.interceptors.EgovInterceptor"/>
</mvc:interceptor>
</mvc:interceptors>
Controller에서 별다른 로직 없이 View를 지정하여 DispatcherServlet에 넘겨주는 작업만 하는 경우가 많다. 이럴 때 사용할 수 있는 것이 바로 <mvc:view-controller>태그이다.
<mvc:view-controller>태그를 설정하여 매핑할 URL패턴과 View이름만 넣어주면 해당 URL을 매핑하고 설정한 view를 리턴하는 ParameterizableViewController가 자동으로 등록된다.
만약 ”/“URL을 요청받았을 경우 “index”를 View이름으로 리턴하기 위해서는 다음과 같이 사용한다.
<mvc:view-controller path="/" view-name="index"/>
InternalResourceViewResolver의 prefix를 /WEB-INF/views/로 정하고 suffix를 .jsp로 정하였다면 / URL이 요청되었을 때 /WEB-INF/views/index.jsp view를 호출하게 된다.
DispatcherServlet은 HandlerMapping를 이용해서 해당 요청을 처리할 Controller를 결정한다. 이 Controller는 요청에 대해서 처리를 하고 데이터를 Model 객체에 반영한다. Spring MVC는 다양한 종류의 Controller를 제공하는데, 데이터 바인딩이나 폼 처리 또는 멀티 액션등의 편의 기능을 제공한다. 이 Controller들은 org.springframework.web.servlet.mvc.Controller 인터페이스를 구현한 클래스들이다.(@Controller는 예외다. 여기서는 @Controller에 대한 설명은 제외한다.) eclipse에서 인터페이스 Controller를 Hierarchy View에서 열어보면 아래와 같은 구조를 보여준다.

이 중 주요 Controller의 용도 및 특징을 표로 나타내면 아래와 같다.
| 클래스 | 용도 및 특징 |
|---|---|
| Controller | 기본적인 Controller 인터페이스이다. Struts의 Action과 비교될 수 있다. Spring에서는 Controller 작성시에 직접 Controller 인터페이스를 구현하지 말고, 아래의 구현 클래스를 확장해서 작성할것을 권장한다. |
| AbstractController | 웹 요청과 응답을 처리하는 기본적인 Controller이다. WebContentGenerator를 상속받기 때문에, HTTP 메소드(POST,GET) 지정, 세션 필수 여부등의 편의기능을 추가로 받는다. |
| AbstractCommandController | HttpServletRequest의 파라미터를 동적으로 데이터 객체(Command)에 바인딩 할 수 있다. 하지만 HTML 폼 처리시엔 SimpleFormController을 사용하라. |
| SimpleFormController | HTML 폼 처리시에 사용하는 Controller이다. AbstractCommandController처럼 HttpServletRequest의 파라미터와 Command 객체를 바인딩할뿐 아니라, 입력폼에 필요한 데이터를 채워 보여주거나(referenceData, formBackingObject), 일반적인 폼 처리 시나리오에 따른 view 분기(formView, successView) 등의 편의 기능을 제공한다. |
| MultiActionController | 연관된 여러 액션을 한 Controller에서 처리할때 사용하는 Controller이다. |
| UrlFilenameViewController | Controller에서 처리 로직이 없이 바로 view로 이동하는 경우에 사용한다. |
단순히 요청을 처리하고 그 결과를 ModelAndView 객체에 반영하는 작업을 할 때는 AbstractController을 상속한 Controller를 구현하면 된다. 구현 Controller에서는 AbstractController의 추상 메소드인 handleRequestInternal을 구현하면 된다.
protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception;
Controller를 작성할때 인터페이스 Controller를 바로 구현하는 대신, AbstractController를 상속받아 구현하면 특정 HTTP 메소드(GET,POST)에 대한 필터링이나 세션 필수 체크등의 편의 기능을 제공받는다. AbstractController의 작업 흐름은 아래와 같다.
관련 프로퍼티를 정리하면 아래와 같다.
| 이름 | 기본값 | 설명 |
|---|---|---|
| supportedMethods | GET,POST | Controller가 지원하는 HTTP 메소드 리스트(GET, POST and PUT)로 콤마(,)로 구분한다. |
| requireSession | false | Controller에서 요청 처리시에 session이 반드시 필요한지 여부이다. 값이 true인데 세션이 없다면 ServletException이 발생한다. |
| cacheSeconds | -1 | 응답의 캐시 헤더에 설정하는 시간값으로 단위는 초단위이다. 값이 0이면 캐시를 수행하지 않는 헤더를 갖게 되며, -1(기본값)이면 어떤 헤더도 생성하지 않으며, 양수값을 설정하면 설정한 값(초)만큼 내용을 캐싱을 수행하는 헤더를 생성한다. |
| synchronizeOnSession | false | 메소드 handleRequestInternal()을 호출할때 세션(HttpSession)에 동기화(synchronized)해서 호출할지 여부이다. 만일 세션이 없다면 아무 영향이 없다. |
사용자 인증처리를 위해 아이디와 패스워드를 입력받는 페이지 예제

<%@ page contentType="text/html; charset=UTF-8"%>
<html>
<head>
<title>Login Page</title>
<link type="text/css" rel="stylesheet" href="scripts/easycompany.css" />
</head>
<body>
<form action="/easycompany/loginProcess.do" method="post">
아이디 : <input type=text name="id"> 패스워드: <input type=password name="password"> <input type=submit value="로그인">
</form>
</body>
</html>
로그인 처리를 하는 LoginController를 AbstractController를 확장해서 작성해 보자. 먼저 아래와 같이 빈설정을 하고
<bean name="/loginProcess.do" class="com.easycompany.controller.hierarchy.LoginController"
p:loginService-ref="loginService"
p:supportedMethods="POST"/> <!--HTTP 메소드가 POST일때만 처리한다-->
메소드 handleRequestInternal()에 실제 구현 로직을 넣어 준다.
package com.easycompany.controller.hierarchy;
...
public class LoginController extends AbstractController{
private LoginService loginService;
public void setLoginService(LoginService loginService){
this.loginService = loginService;
}
/**
* AbstractController의 추상 메소드인 handleRequestInternal의 구현 메소드이다.
* 사용자로 부터 아이디, 패스워드를 입력받아 인증 성공이면 세션 객체에 계정정보를 담고 사원정보리스트 페이지로 포워딩한다.
* 인증에 실패하면 로그인 페이지로 다시 리턴한다.
*/
@Override
protected ModelAndView handleRequestInternal(HttpServletRequest request,
HttpServletResponse response) throws Exception {
String id = request.getParameter("id"); //아이디
String password = request.getParameter("password"); //패스워드
//로그인 인증을 처리한 후, 로그인 성공이면 Account 객체에 계정 관련정보를 리턴한다. 로그인 실패이면 null 리턴.
Account account = (Account) loginService.authenticate(id,password);
if (account != null) { //로그인 성공
request.getSession().setAttribute("UserAccount", account); //계정정보를 세션에 저장.
return new ModelAndView("redirect:/employeeList.do"); //사원리스트 페이지로 이동.
} else { //실패
return new ModelAndView("login"); //로그인페이지로 이동
}
}
}
AbstractCommandController는 요청 파라미터값을 커맨드(Command) 클래스의 필드값과 자동으로 바인딩할 때 사용된다. 커맨드 클래스는 일반적인 JavaBean이면 되는데, ActionForm 같이 프레임워크에 종속적인 구조의 폼 클래스를 사용해야 하는 Struts와의 차이점이라 할 수 있다. 파라미터와 커맨드 클래스의 데이터 바인딩은 일반적으로 알려진 JavaBeans 프로퍼티 표시법을 따른다. firstName 이란 이름의 파라미터가 있다면 커맨드 클래스의 setFirstName([value]) 메소드를 찾아 값을 바인딩한다. 파라미터 address.city 는 커맨드 클래스의 getAddress().setCity([value]) 메소드를 찾아 값을 바인딩한다. 이 기능은 HTML 폼 처리에 유용한 편의 기능이지만, 일반적으로 HTML 폼 처리에는 AbstractCommandController대신 SimpleFormController를 사용한다. AbstractCommandController을 상속받는 구현 Controller에서는 추상메소드 handle()을 구현하면 된다.
protected abstract ModelAndView handle(
HttpServletRequest request, HttpServletResponse response, Object command, BindException errors) throws Exception;
사원번호,부서번호,사원이름등의 검색 조건에 따라 사원리스트를 보여주는 페이지 예제

검색 조건을 담는 빈은 아래와 같다.
package com.easycompany.domain;
public class SearchCriteria {
private String searchEid;
private String searchDid;
private String searchName;
// 위의 변수들에 대한 getter/setter...
}
검색 조건을 화면으로 부터 입력받아 검색 조건 빈(bean)인 SearchCriteria에 담아서 서비스에 넘겨주고 리스트로 결과를 받아오는 EmployeeListController 작성해 보자. EmployeeListController를 AbstractController를 이용해 만든다면, 파라미터의 값을 꺼내고 값을 객체에 담는 코드를 직접 작성해야 한다.
package com.easycompany.controller.hierarchy;
...
public class EmployeeListController extends AbstractController{
....
protected ModelAndView handleRequestInternal(HttpServletRequest request,
HttpServletResponse response) throws Exception {
//request 객체의 파라미터값을 꺼내서
String searchEid = request.getParameter("searchEid"); //사원번호
String searchDid = request.getParameter("searchDid"); //부서번호
String searchName = request.getParameter("searchName"); //사원이름
//객체에 저장한다.
SearchCriteria searchCriteria = new SearchCriteria();
searchCriteria.setSearchEid(searchEid);
searchCriteria.setSearchDid(searchDid);
searchCriteria.setSearchName(searchName);
List<Employee> employeelist = employeeService.getAllEmployees(searchCriteria);
ModelAndView modelview = new ModelAndView();
modelview.addObject("employeelist", employeelist);
modelview.addObject("searchCriteria", searchCriteria);
modelview.setViewName("employeelist");
return modelview;
}
}
맵핑해야할 파라미터가 많다면 상당히 번거로운 작업이고, 단순 작업 코드의 라인이 길어져 코드의 가독성도 떨어진다. EmployeeListController를 AbstractCommandController을 상속받아 구현해 보면 아래와 같이 변경될 것이다.
package com.easycompany.controller.hierarchy;
...
public class EmployeeListController extends AbstractCommandController{
public EmployeeListController(){
//Command 객체에 대한 선언. 빈 설정 파일에 Command 객체에 대한 선언이 있다면 이 코드는 필요없다.
setCommandClass(SearchCriteria.class);
setCommandName("searchCriteria");
}
....
@Override
protected ModelAndView handle(HttpServletRequest request,
HttpServletResponse response, Object command, BindException errors)
throws Exception {
//이미 파라미터와 Command 객체의 바인딩이 되어 있다.
SearchCriteria searchCriteria = (SearchCriteria)command;
List<Employee> employeelist = employeeService.getAllEmployees(searchCriteria);
ModelAndView modelview = new ModelAndView();
modelview.addObject("employeelist", employeelist);
modelview.addObject("searchCriteria", searchCriteria);
modelview.setViewName("employeelist");
return modelview;
}
}
커맨드 클래스 설정은 setCommandClass,setCommandName 메소드 대신에 빈 설정 파일에 정의할 수 있다.
<bean id="employeeListController" class="com.easycompany.controller.hierarchy.EmployeeListController"
p:employeeService-ref="employeeService"
p:commandName="searchCriteria"
p:commandClass="com.easycompany.domain.SearchCriteria"/>
이 데이터 바인딩은 spring의 폼 태그 <form:form> 와 함께 쓰면 더욱 편리하게 사용할 수 있다.
폼 태그의 변수 commandName은 커맨드 클래스의 이름과 일치해야 한다.
커맨드 객체에 사원번호, 부서번호등의 값이 들어 있다면 JSP는 커맨드 객체의 필드값과 폼필드값을 자동으로 바인딩하여 보여주게 된다.
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
...
<form:form commandName="searchCriteria" action="/easycompany/employeeList.do">
<table width="50%" border="1">
<tr>
<td>사원번호 : <form:input path="searchEid"/></td>
<td>부서번호 : <form:input path="searchDid"/></td>
<td>이름 : <form:input path="searchName"/></td>
<td><input type="submit" value="검색" onclick="this.disabled=true,this.form.submit();" /></td>
</tr>
</table>
</form:form>
<table>
<tr>
<th></th>
<th>사원번호</th>
<th>부서번호</th>
<th>이름</th>
<th>나이</th>
<th>이메일</th>
</tr>
<c:forEach items="${employeelist}" var="empinfo">
<tr>
<td></td>
<td><a href="javascript:getEmployeeInfo('${empinfo.employeeid}')">${empinfo.employeeid}</a></td>
<td>${empinfo.departmentid}</td>
<td>${empinfo.name}</td>
<td>${empinfo.age}</td>
<td>${empinfo.email}</td>
</tr>
</c:forEach>
</table>
...
HTML 폼을 보여주거나 전송(submission)하는 등에 폼처리를 다루는 Controller를 작성 한다면, SimpleFormController를 상속한 Controller를 구현하면 된다. SimpleFormController는 상위 클래스인 BaseCommandController와 AbstractFormController가 제공하는 파라미터와 커맨드(폼) 클래스의 데이터 바인딩, 세션 폼 모드, 입력값 검증(validation), 입력폼에 초기 데이터 세팅등의 편의 기능을 그대로 사용하면서 폼 전송시에 결과에 따른 화면 분기(formView, successView)등 편의 기능을 추가로 제공한다. 폼을 보여주고 전송하는 Controller를 각각 만들지 않고 하나로 만들 수 있다. SimpleFormController의 작업 흐름을 보려면 상위 클래스인 AbstractFormController의 handleRequestInternal() 메소드를 참고하면 되는데, 작업 흐름은 아래와 같다.
GET 방식 호출
POST 방식 호출
관련 프로퍼티는 아래와 같다.
| 이름 | 기본값 | 설명 | 해당클래스 |
|---|---|---|---|
| commandName | command | 커맨드 클래스의 이름(별칭) | BaseCommandController |
| commandClass | null | 요청 파라미터와 데이터 바인딩하게 될 커맨드 클래스 | BaseCommandController |
| validators | null | 커맨드 객체의 데이터 유효성검사를 수행할 Validator 빈의 배열 | BaseCommandController |
| validator | null | Validator가 한개인 경우 사용. | BaseCommandController |
| validateOnBinding | true | 유효성검사를 수행할지 여부. true이면 수행한다. | BaseCommandController |
| bindOnNewForm | false | 새로운 폼이 보여지는 시점에서 데이터 바인딩을 할지 여부. | AbstractFormController |
| sessionForm | false | 커맨드 객체를 세션에 저장하여 사용할지 여부. | AbstractFormController |
| formView | null | 사용자가 입력하는 폼페이지나 유효성검사시에 에러났을 경우에 사용하는 뷰를 표시한다. | SimpleFormController |
| successView | null | 폼 제출이 성공했을때 보여줄 뷰를 표시한다. | SimpleFormController |
부서 정보 수정 페이지(/easycompany/webapp/WEB-INF/jsp/modifydepartment.jsp) 예제 이 페이지의 처리 흐름은 아래와 같다.

<form:form commandName="department">
<table>
<tr>
<th>부서번호</th>
<td><c:out value="${department.deptid}"/></td>
</tr>
<tr>
<th>부서이름</th>
<td><form:input path="deptname" size="20"/></td>
</tr>
<tr>
<th>상위부서</th>
<td>
<form:select path="superdeptid">
<option value="">상위부서를 선택하세요.</option>
<form:options items="${deptInfoOneDepthCategory}" />
</form:select>
</td>
</tr>
<tr>
<th>설명</th>
<td><form:textarea path="description" rows="10" cols="40"/></td>
</tr>
</table>
<table width="80%" border="1">
<tr>
<td>
<input type="submit" value="저장"/>
<input type="button" value="리스트페이지" onclick="location.href='/easycompany/departmentList.do?depth=1'"/>
</td>
</tr>
</table>
</form:form>
처리를 담당할 UpdateDepartmentController를 빈 설정 파일(xxx-servlet.xml)에 아래와 같이 등록한다.
<bean id="updateDepartmentController" class="com.easycompany.controller.hierarchy.UpdateDepartmentController"
p:departmentService-ref="departmentService"
p:commandName="department"
p:commandClass="com.easycompany.domain.Department"
p:formView="modifydepartment"
p:successView="redirect:/departmentList.do?depth=1"/>
protected Object formBackingObject(HttpServletRequest request) throws Exception
일반적으로 수정 폼페이지는 기존의 데이터를 폼에 채우고 사용자가 원하는 부분을 수정한 후에 전송(submit)하는데, 기존 데이터를 불러와 폼에 채우는 역할을 formBackingObject 메소드가 담당한다. formBackingObject 메소드는 커맨드 객체를 생성해서 리턴하는데, 필요에 따라 이 메소드를 오버라이드해서 필요한 데이터를 커맨드 객체에 채워 주면 된다. 부서 정보 수정 페이지에서 기존의 부서 정보를 채워서 보여 주는 부분을 먼저 작성해 보자. 오버라이드한 메소드에HTTP GET 메소드 요청이면 파라미터에 있는 부서 아이디로 부서 정보 테이블을 조회해서 결과를 객체 Department에 담아 반환하는 로직을 추가해 보자.
package com.easycompany.controller.hierarchy;
...
public class UpdateDepartmentController extends SimpleFormController{
private DepartmentService departmentService;
public void setDepartmentService(DepartmentService departmentService){
this.departmentService = departmentService;
}
@Override
protected Object formBackingObject(HttpServletRequest request) throws Exception {
if(!isFormSubmission(request)){ // GET 요청이면
String deptid = request.getParameter("deptid");
Department department = departmentService.getDepartmentInfoById(deptid);//부서 아이디로 DB를 조회한 결과가 커맨드 객체 반영.
return department;
}else{ // POST 요청이면
//AbstractFormController의 formBackingObject을 호출하면 요청객체의 파라미터와 설정된 커맨드 객체간에 기본적인 데이터 바인딩이 이루어 진다.
return super.formBackingObject(request);
}
}
...
}
protected Map referenceData(HttpServletRequest request) throws Exception
protected Map referenceData(HttpServletRequest request, Object command, Errors errors) throws Exception
폼 페이지에 미리 보여 주어야 할 데이터중에 커맨드 객체에 포함하기 어려운 경우가 있다. 부서 정보 수정 페이지에 보면 상위 부서 정보가 셀렉트박스로 되어 있는데, 커맨드 객체인 부서(Department)객체에는 해당 부서의 상위부서번호만 있을 뿐 이 회사에 어떤 상위부서들이 있는 지에 대한 정보는 없다. 커맨드 객체에 없지만 페이지에 필요한 이런 참조성 데이터들을 사용하기 위해서 referenceData 메소드를 사용하면 된다. referenceData 메소드는 맵 객체를 반환하는데 이 맵은 모델 객체에 담겨 ModelAndView에 저장된다. 우리는 그저 referenceData 메소드를 오버라이드해서 참조성 데이터를 맵 객체에 넣어 주면 된다.
package com.easycompany.controller.hierarchy;
...
public class UpdateDepartmentController extends SimpleFormController{
private DepartmentService departmentService;
public void setDepartmentService(DepartmentService departmentService){
this.departmentService = departmentService;
}
@Override
protected Map referenceData(HttpServletRequest request, Object command, Errors errors) throws Exception{
Map param = new HashMap();
param.put("depth", "1");
Map referenceMap = new HashMap();
referenceMap.put("deptInfoOneDepthCategory",departmentService.getDepartmentIdNameList(param)); //상위부서정보를 가져와서 Map에 담는다.
return referenceMap;
}
...
}
부서 정보 수정페이지를 열었을때 참조 데이터로 가져온 상위 부서 정보 리스트 중에 해당 부서의 상위 부서값이 셀렉트 박스에서 기본으로 선택(“selected”)되서 보여져야 한다면,
참조데이터 중에 커맨드 객체와 일치하는 값을 일일히 조건문을 사용해서 비교하는 로직을 넣어주는 중노동을 해야 하나,
스프링 폼태그를 사용하면 간단하게 해결된다.
referenceData 메소드가 반환한 상위 부서 정보 맵 데이터중에 해당 부서의 상위부서와 일치하는게 있으면 <form:options>은 해당 옵션을 선택(“selected”)으로 프린트한다.
<form:select path="superdeptid">
<option value="">상위부서를 선택하세요.</option>
<form:options items="${deptInfoOneDepthCategory}" />
</form:select>
protected ModelAndView onSubmit(Object command) throws Exception
protected ModelAndView onSubmit(Object command, BindException errors) throws Exception
protected ModelAndView onSubmit(HttpServletRequest request, HttpServletResponse response, Object command, BindException errors) throws Exception
onSubmit() 메소드를 오버라이드한 메소드를 만들어서 폼의 내용을 전송을 하는 로직을 넣어보자. 커맨드 객체인 부서 정보 객체(Department)의 내용을 DB에 반영하고, 처리가 성공했으면 successView로 이동하고 실패하면 showForm 메소드를 호출한다. 이 showForm 메소드는 폼페이지를 다시 보여주기 위해 필요한 데이터와 에러정보를 ModelAndView에 넣고 formView로 이동한다.
package com.easycompany.controller.hierarchy;
...
public class UpdateDepartmentController extends SimpleFormController{
private DepartmentService departmentService;
public void setDepartmentService(DepartmentService departmentService){
this.departmentService = departmentService;
}
...
@Override
protected ModelAndView onSubmit(HttpServletRequest request,
HttpServletResponse response, Object command, BindException errors) throws Exception{
Department department = (Department) command;
try {
departmentService.updateDepartment(department);
} catch (Exception ex) {
return showForm(request, response, errors);
}
return new ModelAndView(getSuccessView(), "department", department);
}
...
}
지금까지 살펴본 Controller들은 하나의 액션에 하나의 Controller를 만드는 방식이다. (SimpleFormController가 하나의 url에 대해 GET, POST 메소드에 따라 분기 처리를 하기는 하지만.) 연관있는 여러 액션들을 하나의 Controller에 모으고 싶다면 MultiActionController를 상속받아 Controller를 작성하면 되는데 하나의 액션을 하나의 메소드로 작성한다. 그 메소드는 아래의 형식을 갖는다.
public (ModelAndView | Map | String | void) actionName(HttpServletRequest request, HttpServletResponse response);
그러면 어떤 액션(url)을 어떤 메소드가 처리 할지를 결정하는 일이 필요한데, 이때 도움을 주는 인터페이스가 MethodNameResolver이다. MethodNameResolver은 요청에 대해서 어떤 메소드가 처리 할지 메소드 이름을 반환하는데, 스프링은 다음과 같은 3가지 MethodNameResolver 구현 클래스를 제공한다.
상위 부서 리스트를 가져오는 액션과 특정 상위 부서에 속한 하위 부서 리스트를 가져 오는 액션을 처리하는 DepartmentListController를 작성해 보자.
package com.easycompany.controller.hierarchy;
...
public class DepartmentListController extends MultiActionController {
...
//상위 부서 리스트를 가져 온다.
public ModelAndView departmentList(HttpServletRequest request, HttpServletResponse response){
String depth = request.getParameter("depth");
Map paramMap = new HashMap();
paramMap.put("depth", depth);
List<Department> departmentlist = departmentService.getDepartmentList(paramMap);
ModelAndView mav = new ModelAndView("departmentlist");
mav.addObject("departmentlist", departmentlist);
return mav;
}
//특정 상위 부서에 속한 하위 부서 리스트를 가져 온다.
public ModelAndView subDepartmentList(HttpServletRequest request, HttpServletResponse response){
String superdeptid = request.getParameter("superdeptid");
String depth = request.getParameter("depth");
Map paramMap = new HashMap();
paramMap.put("depth", depth);
paramMap.put("superdeptid", superdeptid);
List<Department> departmentlist = departmentService.getDepartmentList(paramMap);
ModelAndView mav = new ModelAndView("departmentsublist");
mav.addObject("departmentlist", departmentlist);
return mav;
}
}
<bean name="/departmentList.do" class="com.easycompany.controller.hierarchy.DepartmentListController"
p:departmentService-ref="departmentService"
p:methodNameResolver-ref="paramResolver"/> <!-- ParameterMethodNameResolver를 MethodNameResolver로 사용-->
<bean id="paramResolver"
class="org.springframework.web.servlet.mvc.multiaction.ParameterMethodNameResolver"
p:paramName="method"/> <!--파라미터이름은 "method"-->
ParameterMethodNameResolver는 어떤 메소드를 호출할지에 대한 정보를 파라미터에 지정하는데, 파라미터 이름은 프로퍼티 “paramName”의 값이다.
<bean id="departmentController" class="com.easycompany.controller.hierarchy.DepartmentListController"
p:departmentService-ref="departmentService"
p:methodNameResolver-ref="pathResolver"/> <!-- InternalPathMethodNameResolver를 MethodNameResolver로 사용-->
<bean id="pathResolver"
class="org.springframework.web.servlet.mvc.multiaction.InternalPathMethodNameResolver"/>
InternalPathMethodNameResolver를 사용하면 URL /abc/foo.do로 요청이 들어올때 public ModelAndView foo(HttpServletRequest, HttpServletResponse) 메소드로 맵핑된다. 예제에서 보면, URL과 메소드간에 맵핑이 아래와 같이 이루어 진다.
<bean id="departmentController" class="com.easycompany.controller.hierarchy.DepartmentListController"
p:departmentService-ref="departmentService"
p:methodNameResolver-ref="propResolver"/> <!-- InternalPathMethodNameResolver를 MethodNameResolver로 사용-->
<bean id="propResolver"
class="org.springframework.web.servlet.mvc.multiaction.PropertiesMethodNameResolver">
<property name="mappings">
<props>
<prop key="/departmentList.do">departmentList</prop>
<prop key="/subDepartmentList.do">subDepartmentList</prop>
</props>
</property>
</bean>
PropertiesMethodNameResolver는 URL과 메소드의 매핑 관계를 “mappings” 프로퍼티에 명시해준다.
UrlFilenameViewController는 Controller에서 처리 로직이 없이 바로 view로 이동하는 경우에 사용하는 Controller이다. DispatcherServlet을 거쳐야 하지만, html 위주의 static한 페이지를 보여줄때 사용한다. 아래와 같이 URL path가 곧 뷰이름이 되며, prefix와 suffix를 지정할수도 있다.
"/index" -> "index"
"/index.html" -> "index"
"/index.html" + prefix "pre_" and suffix "_suf" -> "pre_index_suf"
"/products/view.html" -> "products/view"
Controller를 따로 만들 필요가 없으며, 아래와 같이 빈 설정 파일에서 url과 UrlFilenameViewController를 매핑만 해주면 된다.
<bean id="urlMapping" class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
<property name="mappings">
<props>
<prop key="/login.do">urlFilenameViewController</prop>
<prop key="/validator.do">urlFilenameViewController</prop>
</props>
</property>
</bean>
<bean id="urlFilenameViewController" class="org.springframework.web.servlet.mvc.UrlFilenameViewController" />
InternalResourceViewResolver가 아래와 같이 선언되어 있다면,
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"
p:prefix="/WEB-INF/jsp/" p:suffix=".jsp" />
URL http://localhost:8080/easycompany/login.do로 요청이 들어올때, /easycompany/webapp/WEB-INF/jsp/login.jsp을 찾아서 보여준다.
스프링 프레임워크는 2.5 버젼 부터 Java 5+ 이상이면 @Controller(Annotation-based Controller)를 개발할 수 있는 환경을 제공한다. 인터페이스 Controller를 구현한 SimpleFormController, MultiActionController 같은 기존의 계층형(Hierarchy) Controller와의 주요 차이점 및 개선점은 아래와 같다.
계층형 Controller로 작성된 폼 처리를 @Controller로 구현하는 예도 설명한다. 예제 코드 easycompany의 Controller는 동일한 기능(또한 공통의 Service, DAO, JSP를 사용)을 계층형 Controller와 @Controller로 각각 작성했다.
계층형 Controller들을 사용하면 여러 정보들(요청과 Controller의 매핑 설정 등)을 XML 설정 파일에 명시 해줘야 하는데, 복잡할 뿐 아니라 설정 파일과 코드 사이를 빈번히 이동 해야하는 부담과 번거로움이 될 수 있다. @MVC는 Controller 코드안에 어노테이션으로 설정함으로써 좀 더 편리하게 MVC 프로그래밍을 할 수 있도록 했다. @MVC에서 사용하는 주요 어노테이션은 아래와 같다.
| 이름 | 설명 |
|---|---|
| @Controller | 해당 클래스가 Controller임을 나타내기 위한 어노테이션 |
| @RequestMapping | 요청에 대해 어떤 Controller, 어떤 메소드가 처리할지를 맵핑하기 위한 어노테이션 |
| @RequestParam | Controller 메소드의 파라미터와 웹요청 파라미터와 맵핑하기 위한 어노테이션 |
| @ModelAttribute | Controller 메소드의 파라미터나 리턴값을 Model 객체와 바인딩하기 위한 어노테이션 |
| @SessionAttributes | Model 객체를 세션에 저장하고 사용하기 위한 어노테이션 |
| @RequestPart | Multipart 요청의 경우, 웹요청 파라미터와 맵핑가능한 어노테이션(egov 3.0, Spring 3.1.x부터 추가) |
| @CommandMap | Controller메소드의 파라미터를 Map형태로 받을 때 웹요청 파라미터와 맵핑하기 위한 어노테이션(egov 3.0부터 추가) |
| @ControllerAdvice | Controller를 보조하는 어노테이션으로 Controller에서 쓰이는 공통기능들을 모듈화하여 전역으로 쓰기 위한 어노테이션(egov 3.0, Spring 3.2.X부터 추가) |
@MVC에서 Controller를 만들기 위해서는 작성한 클래스에 @Controller를 붙여주면 된다. 특정 클래스를 구현하거나 상속할 필요가 없다.
package com.easycompany.controller.annotation;
@Controller
public class LoginController {
...
}
앞서 DefaultAnnotationHandlerMapping에서 언급한 대로 <context:component-scan> 태그를 이용해 @Controller들이 있는 패키지를 선언해 주면 된다.
@Controller만 스캔 한다면 include, exclude 등의 필터를 사용하라.
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-2.5.xsd">
<context:component-scan base-package="com.easycompany.controller.annotation" />
</beans>
@RequestMapping은 요청에 대해 어떤 Controller, 어떤 메소드가 처리할지를 맵핑하기 위한 어노테이션이다. @RequestMapping이 사용하는 속성은 아래와 같다.
| 이름 | 타입 | 설명 |
|---|---|---|
| value | String[] | URL 값으로 맵핑 조건을 부여한다. @RequestMapping(value=”/hello.do”) 또는 @RequestMapping(value={”/hello.do”, ”/world.do” })와 같이 표기하며, 기본값이기 때문에 @RequestMapping(”/hello.do”)으로 표기할 수도 있다. ”/myPath/*.do”와 같이 Ant-Style의 패턴매칭을 이용할 수도 있다. Spring 3.1부터 URL뒤에 중괄호를 이용하여 변수값을 직접 받을 수 있도록 하였다. 아래 설명(URI Template Variable Enhancements)을 참고하라 |
| method | RequestMethod[] | HTTP Request 메소드값을 맵핑 조건으로 부여한다. HTTP 요청 메소드값이 일치해야 맵핑이 이루어 지게 한다. @RequestMapping(method = RequestMethod.POST)같은 형식으로 표기한다. 사용 가능한 메소드는 GET, POST, HEAD, OPTIONS, PUT, DELETE, TRACE이다 |
| params | String[] | HTTP Request 파라미터를 맵핑 조건으로 부여한다. params=“myParam=myValue”이면 HTTP Request URL중에 myParam이라는 파라미터가 있어야 하고 값은 myValue이어야 맵핑한다. params=“myParam”와 같이 파라미터 이름만으로 조건을 부여할 수도 있고, ”!myParam”하면 myParam이라는 파라미터가 없는 요청 만을 맵핑한다. @RequestMapping(params={“myParam1=myValue”, “myParam2”, ”!myParam3”})와 같이 조건을 주었다면, HTTP Request에는 파라미터 myParam1이 myValue값을 가지고 있고, myParam2 파라미터가 있어야 하고, myParam3라는 파라미터는 없어야 한다. |
| consumes | String[] | 설정과 Content-Type request헤더가 일치 할 경우에만 URL이 호출된다. |
| produces | String[] | 설정과 Accept request헤더가 일치 할 경우에만 URL이 호출된다. |
@RequestMapping은 클래스 단위(type level)나 메소드 단위(method level)로 설정할 수 있다.
type level
/hello.do 요청이 오면 HelloController의 hello 메소드가 수행된다.
@Controller
@RequestMapping("/hello.do")
public class HelloController {
@RequestMapping //type level에서 URL을 정의하고 Controller에 메소드가 하나만 있어도 요청 처리를 담당할 메소드 위에 @RequestMapping 표기를 해야 제대로 맵핑이 된다.
public String hello(){
...
}
}
method level
/hello.do 요청이 오면 hello 메소드, /helloForm.do 요청은 GET 방식이면 helloGet 메소드, POST 방식이면 helloPost 메소드가 수행된다.
@Controller
public class HelloController {
@RequestMapping(value="/hello.do")
public String hello(){
...
}
@RequestMapping(value="/helloForm.do", method = RequestMethod.GET)
public String helloGet(){
...
}
@RequestMapping(value="/helloForm.do", method = RequestMethod.POST)
public String helloPost(){
...
}
}
type + method level 둘 다 설정할 수도 있는데, 이 경우엔 type level에 설정한 @RequestMapping의 value(URL)를 method level에서 재정의 할수 없다.
/hello.do 요청시에 GET 방식이면 helloGet 메소드, POST 방식이면 helloPost 메소드가 수행된다.
@Controller
@RequestMapping("/hello.do")
public class HelloController {
@RequestMapping(method = RequestMethod.GET)
public String helloGet(){
...
}
@RequestMapping(method = RequestMethod.POST)
public String helloPost(){
...
}
}
AbstractController 상속받아 구현한 예제 코드 LoginController를 어노테이션 기반의 Controller로 구현해 보겠다.
기존의 LoginController는 URL /loginProcess.do로 오는 요청의 HTTP 메소드가 POST일때 handleRequestInternal 메소드가 실행되는 Controller였는데,
다음과 같이 구현할 수 있겠다.
package com.easycompany.controller.annotation;
...
@Controller
public class LoginController {
@Autowired
private LoginService loginService;
@RequestMapping(value = "/loginProcess.do", method = RequestMethod.POST)
public String login(HttpServletRequest request) {
String id = request.getParameter("id");
String password = request.getParameter("password");
Account account = (Account) loginService.authenticate(id,password);
if (account != null) {
request.getSession().setAttribute("UserAccount", account);
return "redirect:/employeeList.do";
} else {
return "login";
}
}
}
위 예제 코드에서 서비스 클래스를 호출하기 위해서 @Autowired가 사용되었는데 자세한 내용은 여기를 참고하라.
type + method level + request
앞의 내용에서 추가되어 request의 header설정 일치 여부에 따라 URL호출이 가능하다.
다음 예제에서 URL이 /pets로 요청된 경우, POST타입의 request의 content-type이 application/json인 경우에만 다음 메소드가 호출된다.
@Controller
...
@RequestMapping(value = "/pets", method = RequestMethod.POST, consumes="application/json")
public void addPet(@RequestBody Pet pet, Model model) {
// implementation omitted
}
다음 예제에서 GET타입으로 URL이 /pets/*로 요청된 경우, request의 accept header가 application/json인 경우에만 다음 메소드가 호출된다.
@Controller
...
@RequestMapping(value = "/pets/{petId}", method = RequestMethod.GET, produces="application/json")
@ResponseBody
public Pet getPet(@PathVariable String petId, Model model)
// implementation omitted
}
URI Template Variale Enhancements
@RequestMapping의 value에 URL뒤에 중괄호로 Controller메소드의 파라미터로 받을 값의 변수명을 입력해주면 변수를 받을 수 있다.
다음 예제에서 Controller메소드의 URL을 ”/user/view/{id}“로 설정하였을 때, 만약 /user/view/12345 로 URL요청이 들어오면 view함수의 파라미터인 id가 12345로 설정된다.
@RequestMapping("/user/view/{id}")
public String view(@PathVariable("id") int id) {
// implementation omitted
}
@RequestParam
@RequestParam은 Controller 메소드의 파라미터와 웹요청 파라미터와 맵핑하기 위한 어노테이션이다. 관련 속성은 아래와 같다.
| 이름 | 타입 | 설명 |
|---|---|---|
| value | String | 파라미터 이름 |
| required | boolean | 해당 파라미터가 반드시 필수 인지 여부. 기본값은 true이다. |
아래 코드와 같은 방법으로 사용되는데, 해당 파라미터가 Request 객체 안에 없을때 그냥 null값을 바인드 하고 싶다면, pageNo 파라미터 처럼 required=false로 명시해야 한다.
name 파라미터는 required가 true이므로, 만일 name 파라미터가 null이면 org.springframework.web.bind.MissingServletRequestParameterException이 발생한다.
@Controller
public class HelloController {
@RequestMapping("/hello.do")
public String hello(@RequestParam("name") String name, //required 조건이 없으면 기본값은 true, 즉 필수 파라미터 이다. 파라미터 name이 존재하지 않으면 Exception 발생.
@RequestParam(value="pageNo", required=false) String pageNo){ //파라미터 pageNo가 존재하지 않으면 String pageNo는 null.
...
}
}
위에서 작성한 LoginController의 login 메소드를 보면 파라미터 아이디와 패스워드를 Http Request 객체에서 getParameter 메소드를 이용해 구하는데,
@RequestParam을 사용하면 아래와 같이 변경할수 있다.
package com.easycompany.controller.annotation;
...
@Controller
public class LoginController {
@Autowired
private LoginService loginService;
@RequestMapping(value = "/loginProcess.do", method = RequestMethod.POST)
public String login(
HttpServletRequest request,
@RequestParam("id") String id,
@RequestParam("password") String password) {
Account account = (Account) loginService.authenticate(id,password);
if (account != null) {
request.getSession().setAttribute("UserAccount", account);
return "redirect:/employeeList.do";
} else {
return "login";
}
}
}
@ModelAttribute의 속성은 아래와 같다.
| 이름 | 타입 | 설명 |
|---|---|---|
| value | String | 바인드하려는 Model 속성 이름. |
@ModelAttribute는 실제적으로 ModelMap.addAttribute와 같은 기능을 발휘하는데, Controller에서 2가지 방법으로 사용된다.
1.메소드 리턴 데이터와 Model 속성(attribute)의 바인딩
메소드에서 비지니스 로직(DB 처리같은)을 처리한 후 결과 데이터를 ModelMap 객체에 저장하는 로직은 일반적으로 자주 발생한다.
...
@RequestMapping(value = "/updateDepartment.do", method = RequestMethod.GET)
public String formBackingObject(@RequestParam("deptid") String deptid, ModelMap model) {
Department department = departmentService.getDepartmentInfoById(deptid); //DB에서 부서정보 데이터를 가져온다.
model.addAttribute("department", department); //데이터를 모델 객체에 저장한다.
return "modifydepartment";
}
...
@ModelAttribute를 메소드에 선언하면 해당 메소드의 리턴 데이터가 ModelMap 객체에 저장된다. 위 코드를 아래와 같이 변경할수 있는데, 사용자로 부터 GET방식의 /updateDepartment.do 호출이 들어오면, formBackingObject 메소드가 실행 되기 전에 DefaultAnnotationHandlerMapping이 org.springframework.web.bind.annotation.support.HandlerMethodInvoker을 이용해서 (@ModelAttribute가 선언된)getEmployeeInfo를 실행하고, 결과를 ModelMap객체에 저장한다. 결과적으로 getEmployeeInfo 메소드는 ModelMap.addAttribute(“department”, departmentService.getDepartmentInfoById(…)) 작업을 하게 되는것이다.
...
@RequestMapping(value = "/updateDepartment.do", method = RequestMethod.GET)
public String formBackingObject() {
return "modifydepartment";
}
@ModelAttribute("department")
public Department getEmployeeInfo(@RequestParam("deptid") String deptid){
return departmentService.getDepartmentInfoById(deptid); //DB에서 부서정보 데이터를 가져온다.
}
또는
public @ModelAttribute("department") Department getDepartmentInfoById(@RequestParam("deptid") String deptid){
return departmentService.getDepartmentInfoById(deptid);
}
...
2.메소드 파라미터와 Model 속성(attribute)의 바인딩
@ModelAttribute는 ModelMap 객체의 특정 속성(attribute) 메소드의 파라미터와 바인딩 할때도 사용될수 있다. 아래와 같이 메소드의 파라미터에 ”@ModelAttribute(“department”) Department department” 선언하면 department에는 (Department)ModelMap.get(“department”) 값이 바인딩된다. 따라서, 아래와 같은 코드라면 formBackingObject 메소드 파라미터 department에는 getDepartmentInfo 메소드가 ModelMap 객체에 저장한 Department 데이터가 들어 있다.
...
@RequestMapping(value = "/updateDepartment.do", method = RequestMethod.GET)
public String formBackingObject(@ModelAttribute("department") Department department) { //department에는 getDepartmentInfo에서 구해온 데이터들이 들어가 있다.
System.out.println(employee.getEmployeeid());
System.out.println(employee.getName());
return "modifydepartment";
}
@ModelAttribute("department")
public Department getDepartmentInfo(@RequestParam("deptid") String deptid){
return departmentService.getDepartmentInfoById(deptid); //DB에서 부서정보 데이터를 가져온다.
}
...
@SessionAttributes는 model attribute를 session에 저장, 유지할 때 사용하는 어노테이션이다. @SessionAttributes는 클래스 레벨(type level)에서 선언할 수 있다. 관련 속성은 아래와 같다.
| 이름 | 타입 | 설명 |
|---|---|---|
| types | Class[] | session에 저장하려는 model attribute의 타입 |
| value | String[] | session에 저장하려는 model attribute의 이름 |
Multipart request의 경우, 넘겨받은 Contents의 Content-Type에 따라 HttpMessageConverter를 통해 해당 타입대로 multipart컨텐츠를 얻을 때 사용하는 어노테이션이다.
예를 들어 다음과 같이 요청이 multipart로 들어올 때
POST /someUrl
Content-Type: multipart/mixed
--edt7Tfrdusa7r3lNQc79vXuhIIMlatb7PQg7Vp
Content-Disposition: form-data; name="meta-data"
Content-Type: application/json; charset=UTF-8
Content-Transfer-Encoding: 8bit
{
"name": "value"
}
--edt7Tfrdusa7r3lNQc79vXuhIIMlatb7PQg7Vp
Content-Disposition: form-data; name="file-data"; filename="file.properties"
Content-Type: text/xml
Content-Transfer-Encoding: 8bit
... File Data ...
Controller에서 요청값은 아래와 같이 받을 수 있다.
@RequestMapping(value="/someUrl", method = RequestMethod.POST)
public String onSubmit(@RequestPart("meta-data") MetaData metadata,
@RequestPart("file-data") MultipartFile file) {
// ...
}
@CommandMap은 실행환경 3.0환경부터 추가된 Controller에서 Map형태로 웹요청 값을 받았을 때 다른 Map형태의 argument와 구분해주기 위한 어노테이션이다. @CommandMap은 파라미터 레벨(type level)에서만 선언할 수 있다.
사용 방법은 다음과 같다.
@RequestMapping("/test.do")
public void test(HttpServletRequest request, @CommandMap Map<String, String> commandMap) {
//생략
}
자세한 사용방법은 AnnotationCommandMapArgumentResolver을 참고한다.
@ControllerAdvice 어노테이션을 통해 Controller에서 쓰이는 몇가지 어노테이션 기능들을 모듈화하여 전역으로 쓸 수 있다. @ControllerAdvice은 @RequestMapping이 붙은 메소드를 지원하며 다음과 같은 Controller 어노테이션을 지원한다.
| 이름 | 설명 |
|---|---|
| @ExceptionHandler | @ExceptionHandler 뒤에 붙은 Exception이 발생했을 때, 전역적으로 예외처리가 가능하다. |
| @InitBinder | 모델 검증과 바인딩을 하기 위한 Annotation으로써 JSR-303 빈 검증기능을 사용하는 스프링 validator를 사용할 수 있다. |
| @ModelAttribute | 도메인 오브젝트나 DTO프로퍼티에 요청파라미터를 한 번에 받을 수 있는 @ModelAttribute를 전역으로 사용 가능하다. |
@ExceptionHandler with @ControllerAdvice
기존에는 예외발생시, AnnotationMethodHandlerExceptionResolver가 Controller내부에서 @ExceptionHandler가 붙은 메소드를 찾아 예외처리를 해준다.
Controller 내부에서만 @ExceptionHandler가 동작하기 때문에 각 Controller별로 @ExceptionHandler 메소드를 만들어야했다.
@Controller
public class HelloController {
@RequestMapping("/hello")
public void hello() {
//DataAccessException이 일어날 가능성
}
// Controller 내부에서 DataAccessException발생시 호출
@ExceptionHandler(DataAccessException.class)
public ModelAndView dataAccessExceptionHandler(DataAccessException e) {
return new ModelAndView("dataexception").addObject("msg", ex.getMessage();
}
}
Spring 3.2부터는 @ControllerAdvice를 이용하여 @ExceptionHandler를 전역으로 쓸 수 있다.
@ControllerAdvice + @ExceptionHandler를 통해 각각의 Exception에 대하여 전역적인 후처리 관리가 가능해진다.
즉, Controller마다 @ExceptionHandler를 만들지 않더라도 @ControllerAdvice가 붙은 Class안에서 여러 Exception에 대한 처리가 가능한 @ExceptionHandler 메소드를 만들면 로지컬한 Exception별 후처리가 가능해지는 것이다.
@ControllerAdvice와 함께 @ExceptionHandler를 쓰는 방법은 다음과 같다. 다음과 같이 쓰는 경우, Controller에서 발생하는 해당 Exception들이 예외처리가 된다.
@ControllerAdvice
public class CentralControllerHandler {
@ExceptionHandler({EgovBizException.class})
public ModelAndView handleEgovBizException(EgovBizException ee) {
//생략
}
@ExceptionHandler({BaseException.class})
public ModelAndView handleBaseException(BaseException be) {
//생략
}
}
@InitBinder with @ControllerAdvice
@ControllerAdvice가 붙은 Class내부에서 @InitBinder 메소드를 씀으로써 이를 전역으로 쓸 수도 있다.
@ControllerAdvice 와 함께 @InitBinder를 쓰는 방법은 다음과 같다. @InitBinder는 Controller에서 @Valid를 쓰는 경우에만 해당 파라미터의 데이터 검증이 적용된다.
Person객체를 모델바인딩 및 검증해주는 PersonValidator를 이용하는 경우이다.
@ControllerAdvice
public class CentralControllerHandler {
@InitBinder
public void initBinder(WebDataBinder binder) {
binder.setValidator(new PersonValidator());
}
//생략
}
이 때 Controller의 구현 예이다.
@RequestMapping(value="/persons")
public void updatePerson(@Valid Person person, HttpServletRequest request, HttpServletResponse response) {
personService.updatePerson(person);
//생략
}
@ControllerAdvice를 통해 @ModelAttribute의 메소드 또한 전역으로 쓰일 수 있다.
@ControllerAdvice
public class GlobalControllerAdvice {
...
@ModelAttribute("model")
public Model getModel() {
return this.model();
}
}
@RequestMapping을 적용한 Controller의 메소드는 아래와 같은 메소드 파라미터와 리턴 타입을 사용할수 있다.
특정 클래스를 확장하거나 인터페이스를 구현해야 하는 제약이 없기 때문에 계층형 Controller 비해 유연한 메소드 시그니쳐를 갖는다.
사용가능한 메소드 파라미터는 아래와 같다.
메소드는 임의의 순서대로 파라미터를 사용할수 있다. 단, BindingResult가 메소드의 argument로 사용될 때는 바인딩 할 커맨드 객체가 바로 앞에 와야 한다.
public String updateEmployee(...,@ModelAttribute("employee") Employee employee,
BindingResult bindingResult,...) /* (O) */
public String updateEmployee(...,BindingResult bindingResult,
@ModelAttribute("employee") Employee employee,...) /* (X) */
이 외의 타입을 메소드 파라미터로 사용하려면?
스프링 프레임워크는 위에서 언급한 타입이 아닌 custom arguments도 메소드 파라미터로 사용할 수 있도록 org.springframework.web.bind.support.WebArgumentResolver라는 인터페이스를 제공한다.
WebArgumentResolver를 사용한 예제는 이곳을 참고
사용가능한 메소드 리턴 타입은 아래와 같다.
@RequestMapping(value = "/updateDepartment.do", method = RequestMethod.GET)
public ModelAndView formBackingObject(@RequestParam("deptid") String deptid) {
Department department = departmentService.getDepartmentInfoById(deptid);
ModelAndView mav = new ModelAndView("modifydepartment");
mav.addObject("department", department);
return mav;
}
또는
public ModelAndView formBackingObject(@RequestParam("deptid") String deptid, ModelMap model) {
Department department = departmentService.getDepartmentInfoById(deptid);
model.addAttribute("department", department);
ModelAndView mav = new ModelAndView("modifydepartment");
mav.addAllObjects(model);
return mav;
}
http://localhost:8080/gamecast/display.html -> display
http://localhost:8080/gamecast/displayShoppingCart.html -> displayShoppingCart
http://localhost:8080/gamecast/admin/index.html -> admin/index
@RequestMapping(value = "/updateDepartment.do", method = RequestMethod.GET)
public Model formBackingObject(@RequestParam("deptid") String deptid, Model model) {
Department department = departmentService.getDepartmentInfoById(deptid);
model.addAttribute("department", department);
return model;
}
또는
@RequestMapping(value = "/updateDepartment.do", method = RequestMethod.GET)
public Model formBackingObject(@RequestParam("deptid") String deptid) {
Department department = departmentService.getDepartmentInfoById(deptid);
Model model = new ExtendedModelMap();
model.addAttribute("department", department);
return model;
}
@RequestMapping(value = "/updateDepartment.do", method = RequestMethod.GET)
public Map formBackingObject(@RequestParam("deptid") String deptid) {
Department department = departmentService.getDepartmentInfoById(deptid);
Map model = new HashMap();
model.put("department", department);
return model;
}
또는
@RequestMapping(value = "/updateDepartment.do", method = RequestMethod.GET)
public Map formBackingObject(@RequestParam("deptid") String deptid, Map model) {
Department department = departmentService.getDepartmentInfoById(deptid);
model.put("department", department);
return model;
}
/* (O) */
@RequestMapping(value = "/updateDepartment.do", method = RequestMethod.GET)
public String formBackingObject(@RequestParam("deptid") String deptid, ModelMap model) {
Department department = departmentService.getDepartmentInfoById(deptid);
model.addAttribute("department", department);
return "modifydepartment";
}
/* (X) */
@RequestMapping(value = "/updateDepartment.do", method = RequestMethod.GET)
public String formBackingObject(@RequestParam("deptid") String deptid) {
Department department = departmentService.getDepartmentInfoById(deptid);
ModelMap model = new ModelMap();
model.addAttribute("department", department);
return "modifydepartment";
}
@MVC는 Controller 개발시에 특정 인터페이스를 구현 하거나 특정 클래스를 상속해야할 필요가 없다.
Controller의 메소드에서 Servlet API를 반드시 참조하지 않아도 되며, 훨씬 유연해진 메소드 시그니쳐로 개발이 가능하다.
여기서는 SimpleFormController의 폼 처리 액션을 @Controller로 구현함으로써, POJO-Style에 가까워졌지만 기존의 계층형 Controller에서 제공하던 기능들을 여전히 구현할 수 있음을 보이고자 한다.
앞서 SimpleFormController을 설명하면서 예제로 작성된 com.easycompany.controller.hierarchy.UpdateDepartmentController를 @ModelAttribute와 @RequestMapping을 이용해서 같은 기능을 @Controller로 작성해 보겠다.
JSP 소스는 동일한 것을 사용한다. 이곳의 예제 화면 이미지 및 JSP 코드를 참고. 기존의 UpdateDepartmentController를 보면 3가지 메소드로 이루어졌다.
package com.easycompany.controller.hierarchy;
...
public class UpdateDepartmentController extends SimpleFormController{
private DepartmentService departmentService;
public void setDepartmentService(DepartmentService departmentService){
this.departmentService = departmentService;
}
//상위부서리스트(selectbox)는 부서정보클래스에 없으므로 , 상위부서리스트 데이터를 DB에서 구해서 별도의 참조데이터로 구성한다.
@Override
protected Map referenceData(HttpServletRequest request, Object command, Errors errors) throws Exception{
Map referenceMap = new HashMap();
referenceMap.put("deptInfoOneDepthCategory",departmentService.getDepartmentIdNameList("1")); //상위부서정보를 가져와서 Map에 담는다.
return referenceMap;
}
@Override
protected Object formBackingObject(HttpServletRequest request) throws Exception {
if(!isFormSubmission(request)){ // GET 요청이면
String deptid = request.getParameter("deptid");
Department department = departmentService.getDepartmentInfoById(deptid);//부서 아이디로 DB를 조회한 결과가 커맨드 객체 반영.
return department;
}else{ // POST 요청이면
//AbstractFormController의 formBackingObject을 호출하면 요청객체의 파라미터와 설정된 커맨드 객체간에 기본적인 데이터 바인딩이 이루어 진다.
return super.formBackingObject(request);
}
}
@Override
protected ModelAndView onSubmit(HttpServletRequest request,
HttpServletResponse response, Object command, BindException errors) throws Exception{
Department department = (Department) command;
try {
departmentService.updateDepartment(department);
} catch (Exception ex) {
return showForm(request, response, errors);
}
return new ModelAndView(getSuccessView(), "department", department);
}
}
@Controller로 작성된 com.easycompany.controller.annotation.UpdateDepartmentController은 3개의 메소드로 이루어져 있다.
계층형 Controller인 기존의 UpdateDepartmentController와는 달리 각 메소드는 Override 할 필요없기 때문에 메소드 이름은 자유롭게 지을 수 있다.
쉬운 비교를 위해 SimpleFormController과 동일한 메소드 이름을 선택했다.
(POJO에 가까운) 프레임워크 코드들은 감춰졌고, 보다 직관적으로 비지니스 내용을 표현할 수 있게 되었다고 생각한다.
package com.easycompany.controller.annotation;
...
@Controller
public class UpdateDepartmentController {
@Autowired
private DepartmentService departmentService;
//상위부서리스트(selectbox)는 부서정보클래스에 없으므로 , 상위부서리스트 데이터를 DB에서 구해서 별도의 참조데이터로 구성한다.
@ModelAttribute("deptInfoOneDepthCategory")
public Map<String, String> referenceData() {
return departmentService.getDepartmentIdNameList("1");
}
// 해당 부서번호의 부서정보 데이터를 불러와 입력폼을 채운다
@RequestMapping(value = "/updateDepartment.do", method = RequestMethod.GET)
public String formBackingObject(@RequestParam("deptid") String deptid, ModelMap model) {
Department department = departmentService.getDepartmentInfoById(deptid);
model.addAttribute("department", department); //form tag의 commandName은 이 attribute name과 일치해야 한다. <form:form commandName="department">.
return "modifydepartment";
}
//사용자가 데이터 수정을 끝내고 저장 버튼을 누르면 수정 데이터로 저장을 담당하는 서비스(DB)를 호출한다.
//저장이 성공하면 부서리스트 페이지로 이동하고 에러가 있으면 다시 입력폼페이지로 이동한다.
@RequestMapping(value = "/updateDepartment.do", method = RequestMethod.POST)
public String onSubmit(@ModelAttribute("department") Department department, BindingResult bindingResult) {
//validation code
new DepartmentValidator().validate(department, bindingResult);
if(bindingResult.hasErrors()){
return "modifydepartment";
}
try {
departmentService.updateDepartment(department);
return "redirect:/departmentList.do?depth=1";
} catch (Exception e) {
e.printStackTrace();
return "modifydepartment";
}
}
}
객체의 유효성 검증을 위해 스프링 프레임워크는 org.springframework.validation.Validator라는 인터페이스를 제공한다. Validator는 특정 계층에 종속적인 구조가 아니라서, web이나 data-access등 어떤 계층의 객체라도 유효성 검증이 가능하게 한다. Jakarta Commons Validator나 Valang 같은 외부 Validator들도 Spring 프레임워크에서 사용할 수 있다. Spring Modules를 이용한 Jakarta Commons Validator 사용 방법에 대해서는 Spring Framework에서 Commons Validator 사용 을 참고하라.
부서 정보를 수정하는 페이지에서 커맨드 객체인 부서 정보 클래스를 유효성 검증하는 코드를 작성해 보자. 부서 클래스인 Department 클래스는 아래와 같다.
package com.easycompany.domain;
public class Department {
private String deptid; //부서아이디
private String deptname; //부서이름
private String superdeptid; //상위부서아이디
private String superdeptname; //상위부서이름
private String depth; //부서레벨
private String description; //부서설명
//위 프로퍼티들의 setter/getter
}
인터페이스 org.springframework.validation.Validator의 메소드는 다음과 같다.
구현 Validator 클래스를 만들때는 위 두 메소드를 구현해야 한다.
Department를 유효성 검증 하기 위한 DepartmentValidator를 만들어 보자. Validation 조건은 부서이름(deptname) 프로퍼티는 반드시 값이 존재해야 하며, 부서설명(description) 프로퍼티는 입력값의 길이가 10 이상이어야 한다.
package com.easycompany.validator;
import org.springframework.validation.Errors;
import org.springframework.validation.Validator;
import com.easycompany.domain.Department;
public class DepartmentValidator implements Validator {
public boolean supports(Class clazz) {
return Department.class.isAssignableFrom(clazz);
}
public void validate(Object target, Errors errors) {
Department department = (Department)target;
if (isEmptyOrWhitespace(department.getDeptname())) { //부서 이름 프로퍼티 값이 존재하는가?
errors.rejectValue("deptname", "required");
}
if (department.getDescription() == null || department.getDescription().length() < 10) { //부서설명 프로퍼티는 값의 길이가 10 이상인가?
errors.rejectValue("description", "lengthsize", new Object[]{10}, "description's length must be larger than 10.");
}
}
public boolean isEmptyOrWhitespace(String value){
if (value == null || value.trim().length() == 0) {
return true;
} else {
return false;
}
}
}
위 코드에서 처럼 유효성 검증이 실패한 경우 Errors 인터페이스의 rejectValue 메소드를 실행하는데, Errors 인터페이스에 대한 자세한 설명은 여기를 참고하라.
errors.rejectValue(“deptname”, “required”);
errors.rejectValue(“description”, “lengthsize”, new Object[]{10}, “description’s length must be larger than 10.”);
스프링에서는 유효성 검증을 위한 ValidationUtils라는 유틸 클래스를 제공한다. 부서 이름 프로퍼티(deptname) 값이 null또는 white space인지 체크하는 부분은 ValidationUtils의 rejectIfEmptyOrWhitespace 메소드를 사용해서 작성할 수 있다.
package com.easycompany.validator;
import org.springframework.validation.Errors;
import org.springframework.validation.Validator;
import org.springframework.validation.ValidationUtils;
import com.easycompany.domain.Department;
public class DepartmentValidator implements Validator {
public boolean supports(Class clazz) {
return Department.class.isAssignableFrom(clazz);
}
public void validate(Object target, Errors errors) {
Department department = (Department)target;
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "deptname", "required");
if (department.getDescription() == null || department.getDescription().length()<10) { //부서설명 프로퍼티는 입력값의 길이가 10 이상인가?
errors.rejectValue("description", "lengthsize", new Object[]{10}, "description's length must be larger than 10.");
}
}
}
message 프로퍼티 파일에서 메시지 key인 “required”, “lengthsize”에 대한 메시지 설정을 한다.
required=필수 입력값입니다.
lengthsize={0}자 이상 입력해야 합니다.
Controller에서 validation을 수행하는 코드를 적용해 보자. 폼을 전송하는 순간에 유효성 검증을 하기 원한다면, com.easycompany.controller.annotation.UpdateDepartmentController 에서 onSubmit 메소드에 validation 코드를 추가한다.
package com.easycompany.controller.annotation;
...
import org.springframework.validation.BindingResult;
import com.easycompany.validator.DepartmentValidator;
@Controller
public class UpdateDepartmentController {
//사용자가 데이터 수정을 끝내고 저장 버튼을 누르면 수정 데이터로 저장을 담당하는 서비스(DB)를 호출한다.
//저장이 성공하면 부서리스트 페이지로 이동하고 에러가 있으면 다시 입력폼페이지로 이동한다.
@RequestMapping(value = "/updateDepartment.do", method = RequestMethod.POST)
public String onSubmit(@ModelAttribute("department") Department department, BindingResult bindingResult) {
//validation code
new DepartmentValidator().validate(department, bindingResult); //validation을 수행한다.
if(bindingResult.hasErrors()){ //validation 에러가 있으면,
return "modifydepartment"; //이 페이지로 이동.
}
try {
departmentService.updateDepartment(department);
return "redirect:/departmentList.do?depth=1";
} catch (Exception e) {
e.printStackTrace();
return "modifydepartment";
}
}
}
손쉬운 에러 메시지 표기를 위해 Spring 폼태그 <form:errors/>를 사용할 것을 권장한다.
/easycompany/webapp/jsp/annotation/modifydepartment.jsp
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
...
<form:form commandName="department">
<table>
...
<tr>
<th>부서이름</th>
<td><form:input path="deptname" size="20"/><form:errors path="deptname" /></td>
</tr>
...
<tr>
<th>설명</th>
<td><form:textarea path="description" rows="10" cols="40"/><form:errors path="description" /></td>
</tr>
</table>
</form:form>
...
부서 이름값을 비우고, 부서설명 부분에 10자 이하로 입력한 후에 저장 버튼을 누르면, 다시 부서정보수정 페이지로 돌아와서 아래와 같이 에러 메시지가 출력될 것이다.

화면처리: validation을 통해 검증방법을 알아보았다. 이전과는 다르게 JSR-303(Bean Validation) 스펙은 자동 검증 방식을 제공한다. @javax.validation.Valid애노테이션을 사용하여 내부적으로(자동으로) 검증이 수행된다.
또한, 최근에 표준 스펙으로 인증받은 JSR-303 빈 검증방식을 이용하여 모델 오브젝트 필드에서 애노테이션을 이용해 검증을 진행할 수 있다.
기존의 검증 방식을 자동 검증 방식으로 변경하였으며, 방법은 컨트롤러 메소드의 @ModelAttribute 파라미터에 @Valid 애노테이션을 추가한다. 그러면 validate() 메소드를 실행하는 대신 바인딩 과정에서 자동으로 검증이 진행된다.
@Controller
public class ExampleController {
@Autowired ExampleValidator validator;
@InitBinder
public void initBinder(WebDataBinder dataBinder){
dataBinder.setValidator(this.validator);
}
@RequestMapping("/insertMember.do")
public String insertMember(@ModelAttribute("memberVO") @Valid MemberVO memberVO, BindingResult bindingResult, ..) {
//..
}
}
위의 방식은 기존의 검증방식을 자동 검증으로 변경한 방법이며 다음에 설명한 검증방법은 제약조건을 빈에 직접 설정하여 검증하는 방식이다.
먼저, 클래스패스에 의존 라이브러리를 추가해야 한다. 메이븐을 사용 중이라면 다음 의존 라이브러리를 프로젝트에 추가한다.
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
<version>1.0.0.GA</version>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>4.0.0.GA</version>
</dependency>
다음은 JSR-303 제약조건 애노테이션이 적용된 모델오브젝트 예제이다.
public class MemberVO{
@NotNull
@Size(min = 1, max = 50, message="이름을 입력하세요.")
private String name;
@Pattern(regexp=".+@.+\\.[a-z]+", message= "이메일 형식이 잘못되었습니다.")
private String email;
//..
}
@NotNull은 빈문자열을 검증하지 못하기 때문에 @Size(min=1)을 사용하여 빈 문자열을 확인해야 한다.
위와같은 제약조건 애노테이션을 사용해 검증을 수행하기 위해서는 LocalValidatiorFactoryBean을 빈으로 등록해 줘야 한다. LocalValidatiorFactoryBean은 JSR-303의 검증기능을 스프링의 Validator처럼 사용할 수 있게 해주는 일종의 어댑터다. LocalValidatiorFactoryBean을 빈으로 등록하면 컨트롤러에서 Validator타입으로 DI 받아서 @InitBinder에서 WebDataBinder에 설정하거나 코드에서 직접 Validator처럼 사용할 수 있다.
<bean id="validator" class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean" />
빈 검증 기능을 validator로 사용하는 컨트롤러 예제이다.
@Controller
public class ExampleController {
@Resource
Validator validator;
@InitBinder
public void initBinder(WebDataBinder dataBinder){
dataBinder.setValidator(this.validator);
}
@RequestMapping("/insertMember.do")
public String insertMember(@ModelAttribute("memberVO") @Valid MemberVO memberVO, BindingResult bindingResult, ..) {
//..
}
}

Controller가 요청에 대한 처리를 하고, View 이름과 데이터(Model)를 ModelAndView에 저장해 DispatcherServlet에 반환(return)하면, DispatcherServlet은 View 이름을 가지고 ViewResolver에게서 실제 View 객체를 얻고, 이 View는 Controller가 저장한 Model 객체의 정보를 출력한다. 여기서는 View와 ViewResolver, 그리고 JSP에서 편리한 데이터 출력을 위해 스프링이 제공하는 Spring form tag library에 대해서 설명한다.
Controller는 코드내에서 실제 View 객체를 생성하지 않고 View 이름만을 결정할 수 있는데, 이로써 Controller와 View의 분리(decoupling)를 가능하게 한다.
package com.easycompany.controller.hierarchy;
...
public class EmployeeListController extends AbstractCommandController{
...
@Override
protected ModelAndView handle(HttpServletRequest request,
HttpServletResponse response, Object command, BindException errors)
throws Exception {
...
List<Employee> employeelist = employeeService.getAllEmployees(commandMap);
ModelAndView modelview = new ModelAndView();
modelview.addObject("employeelist", employeelist);
...
//직접 View 객체를 생성하지 않고,
//View view = new InternalResourceView("/jsp/employeelist.jsp");
//modelview.setView(view);
//View 이름만을 저장.
modelview.setViewName("employeelist");
return modelview;
}
}
이때, DispatcherServlet에 실제 View 객체를 구해주는건 Controller가 아니라 ViewResolver가 담당한다. ViewResolver는 Controller가 반환한 ModelAndView 객체에 담긴 View 이름을 가지고 실제 View 객체를 반환하는 인터페이스이다. Spring에서 제공하는 ViewResolver 구현 클래스는 아래와 같다.
| ViewResolver | 설명 |
|---|---|
| XmlViewResolver | View이름과 View 클래스간의 매핑정보가 담긴 XML로 부터 View이름에 해당하는 View를 구한다. 기본설정 파일은 /WEB-INF/views.xml이다. |
| ResourceBundleViewResolver | View이름과 View 클래스간의 매핑정보가 담긴 리소스 번들(프로퍼티파일)로 부터 View 이름에 해당하는 View를 구한다. 기본설정 파일은 views.properties이다. |
| InternalResourceViewResolver/UrlBasedViewResolver | 특정 디렉토리 경로의 JSP파일들을 호출할 때 편리하게 사용할수 있다. 기본적으로 사용하는 View 클래스는 InternalResourceView이며, View 이름이 곧 JSP 파일이름이 된다. |
| VelocityViewResolver /FreeMarkerViewResolver | Velocity/FreeMarker 연동시에 사용한다. |
비지니스 로직 처리가 끝난 후 ”/jsp/main/abc.jsp” 경로의 JSP 파일로 forwarding하는 Controller가 있다고 하면, InternalResourceViewResolver/UrlBasedViewResolver를 사용해서 아래와 같이 Controller를 작성하고, 빈 정의 파일에 설정할 수 있다.
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"/>
또는
<bean class="org.springframework.web.servlet.view.UrlBasedViewResolver "/>
@Controller
public class HelloController {
@RequestMapping("...")
public String hello(){
... //비지니스 로직 처리.
return "/jsp/main/abc.jsp"; //뷰이름이 곧 JSP 파일의 경로.
}
}
InternalResourceViewResolver/UrlBasedViewResolver의 프로퍼티 prefix, suffix를 사용하면 좀더 간단하게 처리할수 있는데, JSP가 특정 디렉토리 경로 아래에 있고, 예를 들어 /jsp/main 디렉토리 아래, 확장자는 .jsp 이라면,
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"
p:prefix="/jsp/main/" p:suffix=".jsp" />
또는
<bean class="org.springframework.web.servlet.view.UrlBasedViewResolver"
p:prefix="/jsp/main/" p:suffix=".jsp" />
@Controller
public class HelloController {
@RequestMapping("...")
public String hello(){
...
return "abc"; //prefix와 suffix를 제외한 부분만 표기.
}
}
간단히 뷰이름을 설정할 수 있다.
Spring이 제공하는 View 클래스를 사용할 수도 있지만, UI Tool 등과의 연동등으로 인해 View 클래스를 직접 작성해야 하는 경우도 발생한다. 인터페이스 View를 직접 구현해서 View 클래스를 만들수도 있지만, AbstractView를 확장하여 구현해보자. renderMergedOutputModel 메소드를 구현하면 되는데, 아래와 같은 메소드 시그니쳐를 가지고 있다.
protected abstract void renderMergedOutputModel(Map model,
HttpServletRequest request,
HttpServletResponse response) throws Exception
AjaxTags란 Ajax 관련 오픈소스 사용을 위해 Model 객체의 데이터를 ’text/xml’ 형식으로 렌더링하는 View 클래스를 만들어 봤다.
package com.easycompany.view;
import java.io.PrintWriter;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.view.AbstractView;
public class AjaxXmlView extends AbstractView {
@Override
protected void renderMergedOutputModel(Map model,
HttpServletRequest request, HttpServletResponse response)
throws Exception {
response.setContentType("text/xml");
response.setHeader("Cache-Control", "no-cache");
response.setCharacterEncoding("UTF-8");
PrintWriter writer = response.getWriter();
writer.write((String) model.get("ajaxXml"));
writer.close();
}
}
<spring:message>)스프링은 메시지 리소스 파일로 부터 메시지를 가져와 간편하게 출력할수 있도록, <spring:message> 태그를 제공한다.
JSP 페이지의 타이틀을 <spring:message>를 이용해서 출력하는 예제를 만들어 보자.
빈 정의 파일에 리소스 번들 관련된 설정이 되어 있어야 한다.
<!-- Message Source-->
<bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource"
p:basename="messages"/>
먼저 메시지 관련 리소스 파일에 코드값을 설정해준다. PropertiedEditor 같은 유틸의 도움을 받으면 편리하게 한글 입력-편집이 가능하다.
/easycompany/webapp/WEB-INF/classes/messages_ko.properties
...
# -- spring:message --
easaycompany.loginform.title=로그인페이지
easaycompany.employeelist.title=사원 정보 리스트 페이지
easaycompany.updateemployee.title=사원 정보 수정 페이지
easaycompany.insertemployee.title=사원 정보 입력 페이지
easaycompany.departmentlist.title=부서 정보 리스트 페이지
easaycompany.updatedepartment.title=부서 정보 수정 페이지
easaycompany.insertdepartment.title=부서 정보 입력 페이지
JSP 페이지에 커스텀 태그를 사용하기 위해 라이브러리 선언을 해줘야 한다. 그리고 <spring:message> 태그의 code 값에는 메시지 키값을 주면 된다.
...
<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title><spring:message code="easaycompany.departmentlist.title"/></title>
...
해당 화면의 타이틀이 “부서 정보 리스트 페이지”로 표기 될 것이다.
<form:form>,<form:input>,...)폼 관련 어플리케이션을 개발할 때는 스프링이 제공하는 폼 태그와 같이 사용하면 편리하다. 스프링 폼 태그는 Model 데이터의 커맨드 객체(command object)나 참조 데이터(reference data)들을 화면상에서 쉽게 출력하도록 도와 준다. 일단, 스프링 폼 태그를 사용하려면 페이지에 커스텀 태그 라이브러리를 선언해야 한다.
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
스프링 폼 태그에는 아래와 같은 태그들이 있다.
<form:form>는 속성 commandName에 정의된 model attribute를 PageContext에 저장해서, <form:input>이나 <form:hidden>같은 tag들이 접근할 수 있도록 한다.
관련 속성은 아래와 같다.
<form:form> 태그를 사용하고 출력된 페이지에서 소스 보기로 HTML 코드를 열어보면 아래와 같이 HTML FORM 태그가 출력된걸 확인할 수 있을것이다.
<form:form commandName="department" action="http://myUrl..." method="post"> -> <form id="department" action="http://myUrl..." method="post">
<form:form commandName="department"> -> <form id="department" action="현재페이지 URL" method="post">
HTML text타입의 input 태그에 commandName에 지정된 객체 프로퍼티를 바인딩하기 위해 사용한다. path에 프로퍼티 이름을 적으면, text타입의 input 태그의 id, name 값이 프로퍼티 이름이 되고, value는 해당 프로퍼티의 값이 된다.
<form:form commandName="department">
<tr>
<th>부서이름</th>
<td><form:input path="deptname" size="20"/></td>
</tr>
</form:form>
아래와 같이 HTML로 출력된다.
<form id="department" action="/easycompany/updateDepartment.do?deptid=1100" method="post">
<tr>
<th>부서이름</th>
<td><input id="deptname" name="deptname" type="text" value="회식메뉴혁신팀" size="20"/></td>
</tr>
</form>
HTML password타입의 input 태그에 commandName에 지정된 객체 프로퍼티를 바인딩하기 위해 사용한다. 바인딩값을 표기하기 위해서는 showPassword 속성을 showPassword=“true”로 지정해 주어야 한다.
HTML hidden타입의 input 태그에 commandName에 지정된 객체 프로퍼티를 바인딩하기 위해 사용한다.
form:select, form:options, form:option
HTML select, option 태그에 commandName에 지정된 객체 프로퍼티를 바인딩하기 위해 사용한다.
아래와 같이 <form:select>의 path 속성에 commandName 객체의 프로퍼티를 지정하고,
<form:options>의 items 속성에 List, Map등의 Collection 객체를 값으로 주면,
<form:form commandName="department">
<tr>
<th>상위부서</th>
<td>
<form:select path="superdeptid">
<option value="">상위부서를 선택하세요.</option>
<form:options items="${deptInfoOneDepthCategory}" />
</form:select>
</td>
</tr>
</form:form>
아래와 같이 HTML로 출력된다. <form:select>의 path 속성값과 일치하는 option 값이 있으면 selected=“selected” 된다.
<form id="department" action="/easycompany/updateDepartment.do?deptid=1100" method="post">
<tr>
<th>상위부서</th>
<td>
<select id="superdeptid" name="superdeptid">
<option value="">상위부서를 선택하세요.</option>
<option value="5000">금융사업부</option>
<option value="3000">IT연구소</option>
<option value="4000">공공사업부</option>
<option value="1000" selected="selected">경영기획실</option>
<option value="2000">경영지원실</option>
</select>
</td>
</tr>
</form>
<ui:pagination/>
페이징 처리의 편의를 위해 <ui:pagination/> 태그를 제공한다.
<ui:pagination/>의 주요 속성은 아래와 같다.
| 이름 | 설명 | 필수여부 |
|---|---|---|
| paginationInfo | 페이징리스트를 만들기 위해 필요한 데이터. 데이터 타입은 egovframework.rte.ptl.mvc.tags.ui.pagination.PaginationInfo이다. | yes |
| type | 페이징리스트 렌더링을 담당할 클래스의 아이디. 이 아이디는 빈설정 파일에 선언된 프로퍼티 rendererType의 key값이다. | yes |
| jsFunction | 페이지 번호에 걸리게 될 자바스크립트 함수 이름. 페이지 번호가 기본적인 argument로 전달된다. | yes |
ui 태그에 대한 라이브러리 선언을 해주고 페이징 리스트가 위치할 곳에 아래와 같이 사용하면 된다. paginationInfo 속성에는 Controller에서 Model 객체에 저장한 PaginationInfo의 attribute name을 적어 주면 되고, jsFunction 속성은 페이징 리스트의 각 페이지 번호에 걸릴 링크인 자바스크립트 함수명을 적어 주면 된다. type 속성은 빈 설정시에 rendererType 프로퍼티의 entry key값을 적어준다. 렌더링 타입을 태그에서 결정하는 것이다.
1. 관련 클래스 빈 설정한다.
<!-- For Pagination Tag -->
<bean id="imageRenderer" class="com.easycompany.tag.ImagePaginationRenderer"/>
<bean id="textRenderer" class="egovframework.rte.ptl.mvc.tags.ui.pagination.DefaultPaginationRenderer"/>
<bean id="paginationManager" class="egovframework.rte.ptl.mvc.tags.ui.pagination.DefaultPaginationManager">
<property name="rendererType">
<map>
<entry key="image" value-ref="imageRenderer"/>
<entry key="text" value-ref="textRenderer"/>
</map>
</property>
</bean>
2. JSP에서 라이브러리를 선언한 후 사용한다.
<%@ taglib prefix="ui" uri="http://egovframework.gov/ctl/ui"%>
...
<script type="text/javascript">
function linkPage(pageNo){
location.href = "/easycompany/employeeList.do?pageNo="+pageNo;
}
</script>
<body>
...
<ui:pagination paginationInfo = "${paginationInfo}"
type="image"
jsFunction="linkPage"/>
...
</body>
<ui:pagination/>에 대한 좀더 상세한 설명과 사용법, 확장 방법등은 이곳을 참고
Controller에서 화면(JSP) 입력값을 받기 위해서 일반적으로 Command(Form Class) 객체를 사용하지만, Map 객체를 사용하는걸 선호할 수 있다. 전자정부프레임워크 버전 3.0이전에서는 CommandMapArgumentResolver를 통해 Map객체를 사용할 수 있었다. 그러나 3.0부터는 @CommandMap과 AnnotationCommandmapArgumentResolver를 통해 Map객체를 사용할 수 있다. org.springframework.web.method.support.HandlerMethodArgumentResolver의 구현클래스인 AnnotationCommandMapArgumentResolver은 HTTP request 객체에 있는 파라미터이름과 값을 Map 객체에 담아 Controller에서 사용도록 제공한다.
Sping MVC의 @Controller의 메소드의 argument로 사용할 수 있는 유형(이에 관한 정보는 이곳을 참조하라.)은 기존의 계층형 Controller보다 다양해 졌지만,
필요에 따라 기본 유형외의 custom argument를 사용해야 할때가 있을 것이다.
Sping MVC는 Controller의 argument 유형을 customizing 할 수 있는 HandlerMethodArgumentResolver라는 interface를 제공한다.
기존 Spring web 3.1이전 버전에서는 WebArgumentResolver를 구현하여 AnnotationMethodHandlerAdapter에 등록하여 ArgumentResolver를 적용하였으나,
3.1이후부터는 HandlerMethodArgumentResolver를 구현하여 RequestMappingHandlerAdapter에 등록하여 ArgumentResolver를 적용해야 한다.
아래와 같이 Controller의 메소드에서 MySpecialArg라는 custom argument를 argument로 사용하려 한다면,
@Controller
public class HelloController {
public String hello(MySpecialArg mySpecialArg,...) {
...
return "...";
}
}
인터페이스 HandlerMethodArgumentResolver를 구현한 클래스를 만든다. 구현해야 할 메소드는 아래와 같다.
boolean supportsParameter(MethodParameter parameter);
Object resolveArgument(MethodParameter parameter,ModelAndViewContainer mavContainer, NativeWebRequest webRequest,WebDataBinderFactory binderFactory) throws Exception;
public class MySpecialArgumentResolver implements HandlerMethodArgumentResolver{
@Override
public boolean supportsParameter(MethodParameter parameter) {
if(MySpecialArg.class.isAssignableFrom(parameter.getParameterType())) {
return true;
}
else {
return false;
}
}
@Override
public Object resolveArgument(MethodParameter parameter,
ModelAndViewContainer mavContainer, NativeWebRequest webRequest,
WebDataBinderFactory binderFactory) throws Exception {
return new MySpecialArg();
}
}
만들어진 ArgumentResolver클래스는 RequestMappingHandlerAdapter의 customArgumentResolvers 프로퍼티에 등록하도록 한다. list유형 프로퍼티이므로 여러개의 ArgumentResolver클래스를 등록할 수 있다.
<bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter">
<property name="customArgumentResolvers">
<list>
<bean class="... MySpecialArgumentResolver" />
</list>
</property>
</bean>
HTTP request 객체에 있는 파라미터이름과 값을 특정 폼 빈에 담아서 사용하는 방식이 일반적이지만, Map 객체에 담아서 사용하는걸 선호하는 경우도 있다. Controller에서 Map객체를 쓰기 위해 전자정부 프레임워크 3.0부터는 @CommandMap과 AnnotationCommandMapArgumentResolver를 쓰도록 한다.
public String helloPost(@CommandMap Map commandMap, ModelMap model) {
...
}
Map commandMap에 파라미터의 이름과 값이 들어 있게 하려면 위에서 언급한 AnnotationCommandMapArgumentResolver를 이용해야 한다. HandlerMethodArgumentResolver의 구현 클래스인 AnnotationCommandMapArgumentResolver는 Controller 메소드의 argument중에 @CommandMap이 붙은 Map 객체가 있다면, HTTP request 객체에 있는 파라미터이름과 값을 Map객체에 담는다.
package egovframework.rte.ptl.mvc.bind;
...
public class AnnotationCommandMapArgumentResolver implements HandlerMethodArgumentResolver{
@Override
public boolean supportsParameter(MethodParameter parameter) {
if(Map.class.isAssignableFrom(parameter.getParameterType())
&& parameter.hasParameterAnnotation(CommandMap.class)) {
return true;
}
else {
return false;
}
}
@Override
public Object resolveArgument(MethodParameter parameter,
ModelAndViewContainer mavContainer, NativeWebRequest webRequest,
WebDataBinderFactory binderFactory) throws Exception {
Map<String, Object> commandMap = new HashMap<String, Object>();
HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest();
Enumeration<?> enumeration = request.getParameterNames();
while (enumeration.hasMoreElements()) {
String key = (String) enumeration.nextElement();
String[] values = request.getParameterValues(key);
if (values!=null) {
commandMap.put(key, (values.length > 1) ? values:values[0] );
}
}
return commandMap;
}
}
AnnotationCommandMapArgumentResolver를 사용하려면 EgovRequestMappingHandlerAdapter에 등록해야 한다. 보통의 경우는 RequestMappingHandlerAdapter를 등록하여 사용하면 되지만, Controller에 Map객체를 쓰기 위해 AnnotationCommandMapArgumentResolver를 등록하려면 egov3.0부터 제공하는 EgovRequestMappingHandlerAdapter를 사용해야 한다.
만약 RequestMappingHAndlerAdapter를 이용하거나 <mvc:annotation-driven>을 사용할 경우에는 AnnotationCommandMapArgumentResolver를 사용할 수 없으므로 주의해야 한다.
<bean class="egovframework.rte.ptl.mvc.bind.annotation.EgovRequestMappingHandlerAdapter">
<property name="customArgumentResolvers">
<list>
<bean class="egovframework.rte.ptl.mvc.bind.AnnotationCommandMapArgumentResolver" />
</list>
</property>
</bean>
<bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping"/>
테스트를 위한 Form에 데이터를 아래와 같이 등록하고 폼을 제출했을때,

@CommandMap Map 객체의 데이터는 key, value 형태로 아래와 같이 들어 있다. 같은 파라미터 이름으로 여러값이 들어 있는 값은 배열로 들어 있다.
key:text1 value:{aaa,bbb}
key:text2 value:ccc
key:text3 value:ddd
key:cb value:{on,on}
key:rb2 value:on
key:rb1 value:on
스프링 프레임워크, 스프링 웹 MVC를 포함한 기존 웹 프레임워크는 서블릿 API와 서블릿 컨테이너를 위해 개발되었다. 스프링 WebFlux는 5.0 버전부터 추가된 리액티브 스택 웹 프레임워크로서, 서블릿 API와 서블릿 컨테이너를 개발하기 위한 스프링 프레임워크이다. 스프링 웹 MVC를 포함한 기존의 웹 프레임워크와 달리 완전한 논블로킹으로 동작하며 Reactive Streams back pressure를 지원하고 Netty, Undertow, 서블릿 3.1+ 컨테이너 서버에서 실행된다. 웹 프레임워크 모두 스프링 프레임워크에 포함되어 있으며, 원하는 모듈을 선택하여 개발할 수 있다.
스프링 WebFlux가 탄생한 이유 중 하나는 적은 쓰레드로 동시 처리를 제어하고 적은 하드웨어 리소스로 확장하기 위해 논블로킹 웹 스택이 필요했기 때문이다. 이전에도 서블릿 3.1은 논블로킹 I/O를 위한 API를 제공했지만 서블릿으로 논블로킹을 구현하려면 다른 동기 처리나(Filter, Servlet) 블로킹 방식(getParameter, getPart)을 쓰는 API를 사용하기 어려우므로 어떤 논블로킹과도 잘 동작하는 새로운 공통 API를 만들게 됐다. 또한 이미 비동기 논블로킹 환경에서 자리를 잡은 서버(e.g. Netty) 때문에라도 새 API가 필요했다.
또 다른 이유는 함수형 프로그래밍으로 자바 8에서 추가된 람다 표현식으로 자바에서도 함수형 API를 작성할 수 있게 되어 continuation-style API로 비동기 로직을 선언적으로 작성할 수 있다.
리액티브라는 용어는 변화에 반응하는 것을 중심에 두고 만든 프로그래밍 모델을 의미한다. 논블로킹은 작업을 기다리기보단 완료되거나 데이터를 사용할 수 있게 되면 반응하므로 논블로킹도 리액티브이다. 스프링은 리액티브와 관련한 중요한 메커니즘이 하나 더 있는데, 논블로킹 back pressure이다. 동기식 명령형 코드에서 블로킹 호출은 호출자가 대기하도록 하는 자연스러운 형태의 back pressure 역할을 한다. 논블로킹 코드에서는 프로듀서 속도가 컨슈머 속도롤 압도하지 않도록 이벤트 속도를 제어하는 것이 중요하다.
리액티브 스트림은 비동기 구성 요소 간의 상호 작용을 정의하는 간단한 사양(Java 9에서도 채택됨)으로, back pressure가 있는 비동기 구성 요소 간의 상호 작용을 정의한다. 예를 들어 데이터 저장소(Publisher 역할)가 데이터를 생성하면 HTTP 서버(Subscriber 역할)가 이 데이터로 요청을 처리할 수 있다. 리액티브 스트림의 주요 목적은 Subscriber가 Publisher의 데이터 생성 속도를 제어할 수 있도록 하는 것이다.
자주 묻는 질문 : Publisher 속도를 늦출 수 없으면 어떻게 할까?
리액티브 스트림의 목적은 메커니즘과 경계를 확립하는 것이다. Publisher가 속도를 늦출 수 없다면 데이터를 버퍼에 담을지(Buffer), 데이터를 삭제할지(Drop), 실패(Fail)로 처리할지를 결정해야 한다.
리액티브 스트림은 상호 운용성을 위해 중요한 역할을 한다. 하지만 이건 라이브러리와 인프라 구조에 사용되는 컴포넌트에는 유용하지만 애플리케이션 API에서 다루기엔 너무 저수준이다. 애플리케이션은 컬렉션 뿐만 아니라 비동기 로직을 구성하기 위해 더 높은 수준의 더 풍부하고 기능적인 API가 필요하다. 이는 Java 8 Stream API와 유사하며 이것이 바로 리액티브 라이브러리가 하는 역할이다.
Reactor는 스프링 WebFlux가를 위해 선택된 리액티브 라이브러리이다. 이 라이브러리는 0…1(Mono) 및 0…N(Flux)의 데이터 시퀀스에서 작업할 수 있는 Mono 및 Flux API 유형을 제공하며, ReactiveX의 연산자 어휘와 일치하는 풍부한 연산자 집합을 통해 작동한다. Reactor는 리액티브 스트림 라이브러리이므로 모든 연산자는 논블로킹 back pressure를 지원한다. Reactor는 서버 측 Java에 중점을 두고 스프링과 긴밀히 협력하여 개발되었다.
WebFlux는 핵심 종속성으로 Reactor를 필요로 하지만, 리액티브 스트림를 통해 다른 리액티브 라이브러리와 상호 운용할 수 있다. 일반적으로 WebFlux API는 일반 Publisher를 입력으로 받아 내부적으로 Reactor 유형에 맞게 조정하고 이를 사용한 후 Flux 또는 Mono를 출력으로 반환한다. 따라서 모든 Publisher를 입력으로 전달할 수 있고 출력에 연산을 적용할 수 있지만 다른 리액티브 라이브러리와 함께 사용하려면 출력을 조정해야 한다. 가능한 경우(예: 주석이 달린 컨트롤러) WebFlux는 RxJava나 다른 리액티브 라이브러리에 맞게 바꿔준다. 자세한 내용은 리액티브 라이브러리를 참조하라.
스프링 웹 모듈에 있는 WebFlux는 여러 서버를 지원하기 위한 HTTP 추상화와 리액티브 스트림 어댑터, 코덱, Servlet APT에 상응하는 핵심 웹 핸들러 API 등 스프링 WebFlux의 기반이 되는 리액트브 기반이 포함되어 있다. 이러한 기분 위에서 스프링 WebFlux는 두 가지 프로그래밍 모델 중 하나를 선택할 수 있다.
스프링 MVC와 WebFlux 중 어떤 것을 적용할 것이냐는 이분법적 사고는 좋지 않다. 사실, 두 가지 모두 함께 작동하여 사용 가능한 옵션의 범위를 확장한다. 이 둘은 서로의 연속성과 일관성을 위해 설계되었으며, 나란히 사용할 수 있고, 각 측면의 피드백은 양쪽 모두에게 도움이 된다. 다음 다이어그램은 이 두 가지의 관계, 공통점, 그리고 각각 고유하게 지원하는 기능을 보여준다.
다음과 같은 구체적인 사항을 고려하는 것이 좋다.

스프링 WebFlux는 Tomcat, Jetty, Servlet 3.1+ 컨테이너 뿐만 아니라 Netty 및 Undertow도 지원된다. 모든 서버는 낮은 수준의 공통 API에 맞춰 조정되므로 서버 전반에서 상위 수준의 프로그래밍 모델을 지원할 수 있다.
스프링 WebFlux에는 서버 기동이나 중단을 위한 내장 기능은 없다. 하지만 스프링 설정과 WebFlux를 조립해 구성한 코드로 쉽게 애플리케이션을 실행할 수 있다.
스프링 Boot에는 이러한 단계를 자동화하는 WebFlux 스타터가 있다. 기본적으로 스타터는 Netty를 사용하지만, Maven 또는 Gradle 종속성을 변경하여 Tomcat, Jetty 또는 Undertow로 쉽게 전환할 수 있다. 스프링 Boot가 Netty를 디폴트로 사용하는 이유는 보통 비동기 논블로킹에 많이 사용되기도 하고 클라이언트와 서버가 리소스를 공유할 수 있어서다.
Tomcat과 Jetty는 스프링 MVC와 WebFlux에 모두 사용할 수 있다. 하지만 동작방식은 다르다는 점에 주의해야 한다. 스프링 MVC는 Servlet 차단 I/O에 의존하며, 애플리케이션이 필요한 경우 Servlet API를 직접 사용할 수 있다. 스프링 WebFlux는 Servlet 3.1 논블로킹 I/O롤 동작하며 서블릿 API는 저수준 어댑터로 사용하기 때문에 노출되어 있지 않다.
스프링 WebFlux에서 Undertow를 사용할 때는 서블릿 API가 아닌 Undertow API를 사용한다.
성능에는 많은 특징과 의미가 있다. 리액티브 및 논블로킹은 일반적으로 애플리케이션을 더 빠르게 실행하지 않는다. 일부 경우(예: 웹클라이언트를 사용하여 원격 호출을 병렬로 실행하는 경우)에는 그럴 수 있다. 전반적으로 논블로킹 방식으로 작업을 수행하려면 더 많은 작업이 필요하며 필요한 처리 시간이 약간 늘어날 수 있다.
리액티브 및 논블로킹 방식의 주요 이점은 적은 수의 고정된 스레드와 적은 메모리로 확장할 수 있다는 점이다. 따라서 애플리케이션이 보다 예측 가능한 방식으로 확장되므로 부하가 걸렸을 때 복원력이 향상된다. 그러나 이러한 이점을 누리려면 느리고 예측할 수 없는 네트워크 I/O가 혼합된 경우 등 지연 시간이 어느 정도 발생해야 한다. 바로 이 지점에서 리액티브 스택의 강점이 드러나기 시작하며, 그 차이는 극적일 수 있다.
스프링 MVC와 스프링 WebFlux는 모두 Annotated Controller를 사용할 수 있다는 점은 동일해도 동시성 모델과 블로킹/스레드 기본 전략은 다르다. 스프링 MVC(그리고 일반적인 서블릿 애플리케이션)는 애플리케이션이 처리중인 스레드가 잠시 중단될 수 있다(예를 들어 원격 호출의 경우). 이러한 이유로 서블릿 컨테이너는 요청 처리 중에 잠재적인 차단을 흡수하기 위해 대규모 스레드 풀을 사용한다. 스프링 WebFlux(및 일반적으로 논블로킹 서버)에서는 실행되는 스레드가 중단되지 않는다는 전제가 있다. 따라서 논블로킹 서버는 작은 고정 크기의 스레드 풀(이벤트 루프 워커)을 사용하여 요청을 처리한다.
"확장"과 "적은 수의 스레드"는 모순적으로 들릴 수 있지만 현재 스레드를 차단하지 않고 대신 콜백에 의존한다는 것은 흡수할 차단 호출이 없기 때문에 추가 스레드가 필요하지 않다는 것을 의미한다.
블로킹 라이브러리를 사용해야 한다면 어떻게 해야 할까? Reactor와 RxJava는 모두 다른 스레드에서 처리를 계속할 수 있는 PublishOn 연산자를 제공한다. 즉, 쉽게 빠져나갈 수 있는 탈출구가 있다는 뜻이다. 그러나 블로킹 API는 이 동시성 모델에 적합하지 않다는 점에 유의해야 한다.
Reactor와 RxJava에서는 연산자를 통해 로직을 선언한다. 런타임에 데이터가 별개의 단계에서 순차적으로 처리되는 리액티브 파이프라인이 형성된다. 이 파이프라인의 주요 이점은 해당 파이프라인 내의 애플리케이션 코드가 동시에 호출되지 않기 때문에 애플리케이션이 변경 가능한 상태를 보호할 필요가 없다는 것이다.
스프링 WebFlux를 사용하는 애플리케이션은 어떤 스레드를 얼마나 실행할까?
스프링 프레임워크에서 서버를 직접 실행하거나 중단할 수 없다. 서버의 스레딩 모델을 구성하려면 서버별 구성 API를 사용하거나 스프링 Boot를 사용하는 경우 각 서버에 대한 스프링 Boot 구성 옵션을 확인해야 한다. WebClient를 직접 구성할 수도 있다. 다른 라이브러리의 경우 해당 라이브러리 설명서를 참고하라.
스프링-웹 모듈에는 반응형 웹 애플리케이션을 위한 다음과 같은 기본 지원이 포함되어 있다.
HttpHandler는 요청과 응답을 처리하는 메소드를 하나만 가지고 있다. 의도적으로 최소한의 기능을 제공하며, 주된 목적은 다양한 HTTP 서버 API에 대한 최소한의 추상화이다. 지원하는 서버의 API는 아래 표와 같다.
| 서버 이름 | 사용하는 Servlet API | 리액티브 스트림 지원 |
|---|---|---|
| Netty | Netty API | Reactor Netty |
| Undertow | Undertow API API | spring-web: Undertow to 리액티브 스트림 브릿지 |
| Tomcat | 서블릿 3.1 논블로킹 I/O; ByteBuffers로 byte[]를 읽고 쓰는 Tomcat API | spring-web: 서블릿 3.1 논블로킹 I/O to 리액티브 스트림 브릿지 |
| Jetty | 서블릿 3.1 논블로킹 I/O; ByteBuffers로 byte[]를 읽고 쓰는 Jetty API | spring-web: 서블릿 3.1 논블로킹 I/O to 리액티브 스트림 브릿지 |
| 서블릿 3.1 컨테이너 | 서블릿 3.1 논블로킹 I/O | spring-web: 서블릿 3.1 논블로킹 I/O to 리액티브 스트림 브릿지 |
서버 Dependency는 아래 표와 같다(지원 버전 참고)
| 서버 이름 | Group ID | Artifact Name |
|---|---|---|
| Reactor Netty | io.projectreactor.netty | reactor-netty |
| Undertow | io.undertow | undertow-core |
| Tomcat | org.apache.tomcat.embed | tomcat-embed-core |
| Jetty | org.eclipse.jetty | jetty-server, jetty-servlet |
다음은 각 서버의 API 어댑터를 활용하는 HttpHandler이다.
HttpHandler handler = ...
ReactorHttpHandlerAdapter adapter = new ReactorHttpHandlerAdapter(handler);
HttpServer.create().host(host).port(port).handle(adapter).bind().block();
HttpHandler handler = ...
UndertowHttpHandlerAdapter adapter = new UndertowHttpHandlerAdapter(handler);
Undertow server = Undertow.builder().addHttpListener(port, host).setHandler(adapter).build();
server.start();
HttpHandler handler = ...
Servlet servlet = new TomcatHttpHandlerAdapter(handler);
Tomcat server = new Tomcat();
File base = new File(System.getProperty("java.io.tmpdir"));
Context rootContext = server.addContext("", base.getAbsolutePath());
Tomcat.addServlet(rootContext, "main", servlet);
rootContext.addServletMappingDecoded("/", "main");
server.setHost(host);
server.setPort(port);
server.start();
HttpHandler handler = ...
Servlet servlet = new JettyHttpHandlerAdapter(handler);
Server server = new Server();
ServletContextHandler contextHandler = new ServletContextHandler(server, "");
contextHandler.addServlet(new ServletHolder(servlet), "/");
contextHandler.start();
ServerConnector connector = new ServerConnector(server);
connector.setHost(host);
connector.setPort(port);
server.addConnector(connector);
server.start();
서블릿 3.1+ 컨테이너에 WAR를 배포하려면 WAR에 AbstractReactiveWebInitializer를 확장하여 추가하면 된다. 이 클래스는 HttpHandler, ServletHttpHandlerAdapter를 감싸고 있으며, 이 핸들러를 서블릿으로 등록한다.
org.springframework.web.server 패키지를 보면 HttpHandler가 WebHandler와 여러 WebExceptionHandler, WebFilter로 체인을 형성해 요청을 처리하는 범용 웹 API를 제공한다. WebHttpHandlerBuilder에 컴포넌트를 등록하거나 스프링 ApplicationContext 위치만 알려주면 자동으로 컴포넌트 체인에 추가한다. HttpHandler는 서로 다른 HTTP 서버를 쓰기 위한 추상화가 전부인 반면, WebHandler API는 아래와 같이 웹 애플리케이션에서 흔히 쓰는 광범위한 기능을 제공한다.
ServerWebExchange는 form 데이터에 접근할 수 있는 메소드를 제공한다.
Mono<MultiValueMap<String, String>> getFormData();
DefaultServerWebExchange는 설정에 있는 HttpMessageReader를 사용해 form 데이터를 MultiValueMap으로 파싱한다. 디폴트로 사용하는 리더는 ServerCodecConfigurer 빈에 있는 FormHttpMessageReader이다.
ServerWebExchange는 multipart 데이터에 접근할 수 있는 메소드를 제공한다.
Mono<MultiValueMap<String, Part>> getMultipartData();
DefaultServerWebExchange는 설정에 있는 HttpMessageReader<MultiValueMap<String, Part»를 사용해 multipart/form-data 컨텐츠를 MultiValueMap으로 파싱한다. 현재로써는 Synchronoss NIO Multipart가 유일하게 지원하는 서브파티 라이브러리이며, 논블로킹으로 multipart 요청을 파싱하는 유일한 라이브러리이다. ServerCodecConfigurer 빈으로 활성화할 수 있다.
스트리밍 방식으로 multipart 데이터를 파싱하려면 HttpMessageReader
Load Balancers와 같이 프톡시를 경유한 요청은 호스트, 포트, URL 스킴이 변경될 수 있어서 클라이언트 입장에서는 원래의 URL 정보를 알아내기 어렵다. RFC 7239 정의에 따르면 Forwarded HTTP 헤더는 프록시가 원래 요청에 대한 정보를 추가하는 헤더이다. X-Forwarded-Host, X-Forwarded-Port, X-Forwarded-Proto, X-Forwarded-Ssl, and X-Forwarded-Prefix 같은 비표준 헤더도 존재한다. ForwardedHeaderTransformer는 forwarded 헤더를 보고 요청의 호스트, 포트, 스킴을 바꿔준 다음, 헤더를 제거하는 컴포넌트이다. ForwardedHeaderTransformer라는 이름으로 빈을 정의하면 자동으로 체인에 추가된다. forwarded 헤더는 보안에 신경써야 할 요소가 있는데 프록시가 헤더를 추가한건지, 클라이언트가 악의적으로 추가한 것인지 애플리케이션에서는 알 수 없기 때문이다. 따라서 외부에서 들어오는 신뢰할 수 없는 프록시 요청을 제거할 때 ForwardedHeaderTransformer를 removeOnly=true로 설정하여 헤더 정보를 사용하지 않고 제거할 수 있다.
5.1 버전부터 ForwardedHeaderFilter는 제거대상(deprecated)이 되어 ForwardedHeaderTransformer를 대신한다. 따라서 exchange(http 요청/응답과 세션 정보 등의 컨테이너)를 만들기 전에 forwarded 헤더를 처리할 수 있다. 필터를 사용하더라도 이 필터는 전체 필터 리스트에서 제외되며 그 대신 ForwardedHeaderTransformer를 사용한다.
WebHandler API에서는 WebFilter를 사용하면 다른 필터 체인과 WebHandler 전후에 요청을 가로채서 원하는 로직을 넣을 수 있다. WebFilter를 등록하려면 스프링 빈으로 만들어 빈 위에 @Order를 선언아거나 Ordered를 구현해 순서를 정해도 되고, WebFlux Config를 사용해도 된다.
CORS는 컨트롤러에 어노테이션을 선언하는 것으로 잘 동작한다. Spring Security와 사용하면 CorsFilter를 사용해서 Spring Security 필터 체인보다 먼저 처리하도록 해야 한다.
WebHandler API에서는 WebFilter 체인과 WebHandler에서 발생한 예외를 WebExceptionHandler로 처리한다. WebExceptionHandler를 등록하려면 스프링 빈으로 빈 위에 @Order를 선언하거나 Ordered를 구현해 순서를 정해도 되고, WebFlux Config를 사용해도 된다. 아래 표는 바로 사용할 수 있는 WebExceptionHandler 구현체이다.
| Exception Handler | Description |
|---|---|
| ResponseStatusExceptionHandler | Http status code를 지정할 수 있는 ResponseStatusException을 처리한다. |
| WebFluxResponseStatusExceptionHandler | ResponseStatusExceptionHandler를 확장하는 것으로 다른 exception 타입도 @ResponseStatus를 선언해서 HTTP status code를 정할 수 있다.이 핸들러는 WebFlux Config 안에 선언되어 있다. |
스프링 WebFlux도 스프링 MVC와 유사한 프론트 컨트롤러 패턴을 사용한다. 중앙 WebHandler가 받은 요청을 다른 컴포넌트에 위임하는데 DispatcherHandler가 바로 이 중앙 WebHandler다. 이 모델 덕분에 다양한 워크플로우를 지원할 수 있다. DispatcherHandler는 스프링 설정에 따라 그에 맞는 컴포넌트로 위임한다. DispatcherHandler도 스프링 빈이며 ApplicationContextAware 인터페이스를 구현했기 때문에 실행중인 컨텍스트에 접근할 수 있다. DispatcherHandler 빈을 WebHandler 이름으로 정의하면 WebHttpHandlerBuilder가 감지하고 WebHandler API에서 설명했던 체인에 추가한다.
WebFlux 애플리케이션에서 사용하는 일반적인 스프링 설정은 다음과 같다.
WebExceptionHandler가 체인을 만들 때 아래와 같이 사용한다.
ApplicationContext context = ...
HttpHandler handler = WebHttpHandlerBuilder.applicationContext(context).build();
DispatcherHandler는 요청을 처리하고 그에 맞는 응답을 만들 때 사용하는 특별한 빈이 있다. 특별한 빈이란 WebFlux 프레임워크가 동작하는데 필요하며 스프링이 관리하는 Object 인스턴스를 말한다. 이 빈들은 기본적으로 내장돼 있지만 프로퍼티를 수정해서 확장하거나 커스텀 빈으로 대체할 수도 있다.
DispatcherHandler는 다음과 같은 빈을 감지한다. 저수준에서 동작하는 다른 빈도 자동으로 추가될 수 있다는 점에 주의하라
| Bean type | Explanation |
|---|---|
| HandlerMapping | 요청을 핸들러에 매핑한다. 매핑 기준은 HandlerMapping 구현체마다 다른다(어노테이션을 선언한 컨트롤러, URL 패턴 매칭 등)주로 쓰는 구현체는 @RequestMapping을 선언한 메소드를 찾는 RequestMappingHandlerMapping, 함수형 엔드포인트를 라이팅하는 RouterFunctionMapping, URL 패스 패턴으로 WebHandler를 찾는 SimpleUrlHandlerMapping 등이 있다. |
| HandlerAdapter | 핸들러가 실제로 호출되는 방식에 관계없이 요청에 매핑된 핸들러를 호출할 수 있도록 DispatcherHandler를 지원한다.예를 들어 어노테이션이 있는 컨트롤러를 호출하려면 어노테이션을 해결해야 한다.HandlerAdapter의 주요 목적은 이러한 세부 사항으로부터 DispatcherHandler를 보호하는 것이다. |
| HandlerResultHandler | 핸들러 호출의 결과를 처리하고 응답을 종료한다. Result Handling을 참고하라. |
애플리케이션은 요청을 처리하는 데 필요한 인프라 빈(웹 핸들러 API 및 DispatcherHandler)을 선언할 수 있다. 그러나 대부분의 경우 WebFlux Config로 시작하는게 좋다. 이 구성은 필요한 빈을 선언하고 이를 사용자 정의할 수 있는 상위 수준의 구성 콜백 API를 제공한다.
스프링 부트를 사용해도 이 WebFlux Config로 초기화하며 부트가 제공하는 옵션으로 좀 더 편리하게 설정을 관리할 수 있다.
DispatcherHandler는 다음과 같이 요청을 처리한다.
핸들러 호출의 반환 값은 HandlerAdapter를 통해 몇 가지 추가 컨텍스트와 함께 HandlerResult로 래핑되어 지원을 요청하는 첫 번째 HandlerResultHandler로 전달된다. 아래 표는 사용 가능한 HandlerResultHandler 구현을 보여 주며, 모두 WebFlux 구성에 선언되어 있다.:
| Result Handler Type | Return Values | Default Order |
|---|---|---|
| ResponseEntityResultHandler | ResponseEntity, 보통은 @Controller에서 사용 | 0 |
| ServerResponseResultHandler | ServerResponse, 보통은 Functional Endpoints에서 사용 | 0 |
| ResponseBodyResultHandler | @ResponseBody, @RestController에서 리턴한 값을 처리 | 100 |
| ViewResolutionResultHandler | CharSequence, View, Model, Map, Rendering이나 다른 Object를 model attribute로 처리 | Integer.MAX_VALUE |
HandlerAdapter가 리턴한 HandlerResult는 핸들러마다 다른 에러 처리 함수에 넘겨진다. 이 함수는 아래와 같을 때 호출된다.
핸들러가 리턴한 리액티브 타입이 데이터를 produce 하기 전에 에러를 알아차릴 수 있으면 이 함수로 응답을 변경할 수 있다.(예를 들어 status로) 이로 인해 @Controller 클래스의 특정 메소드에 @ExceptionHandler를 선언할 수 있다. 스프링 MVC에선 HandlerExceptionResolver가 이 역할을 담당한다. 여기서 중요한 것은 MVC가 아니지만 WebFlux 핸들러를 선택하기 전 발생한 Exception은 @ControllerAdvice로 처리할 수 없다.
뷰 해상도를 사용하면 특정 뷰 기술에 종속되지 않고 HTML 템플릿과 모델을 사용하여 브라우저에 렌더링할 수 있다. WebFlux에서 뷰 해상도는 ViewResolver 인스턴스를 사용하여 String을 View 인스턴스에 매핑하는 전용 HandlerResultHandler를 통해 지원된다. 그런 다음 뷰를 사용하여 응답을 렌더링한다.
ViewResolutionResultHandler로 전달된 HandlerResult에는 핸들러의 반환 값과 요청 처리 중에 추가된 속성이 포함된 모델이 포함된다. 반환 값은 다음 중 하나로 처리된다.
모델에는 비동기 리액티브 타입도 있을 수 있다(예를 들어 리액터나 RxJava가 리턴한 값). 이런 model attribute는 AbstractView가 랜더링하기 전에 실제값으로 바꿔준다.
single-value 리액티브 타입은 비어있지 않다면 값 하나로 리졸브되고 multi-value 리액티브 타입(에를 들어 Flux
뷰 리졸브는 스프링 설정에 ViewResolutionResultHandler만 추가하면된다. WebFlux Config는 뷰 리졸브를 위한 설정 API를 제공한다.
리액티브는 뷰 이름에 redirect: 프리픽스를 붙이기만 하면 된다. UrlBasedViewResolver(히위 클래스도 포함)가 이를 리다이렉트 요청으로 판단한다. 프리픽스를 제외한 나머지 뷰 이름은 리다이렉트 URL로 사용한다. 동작 자체는 컨트롤러가 RedirectView나 Rendering.redirectTo(“abc”).build()를 리턴했을 때와 동일하지만, 이 방법은 컨트롤러가 직접 뷰 이름을 보고 처리한다. redirect:/some/resource 같은 값은 현재 애플리케이션에서 이동할 페이지를 찾고 redirect:https://example.com/arbitrary/path 같이 사용하면 해당 URL로 리다이렉트한다.
content negotiation은 ViewResolutionResultHandler가 담당한다. 요청 미디어 타입과 View가 지원하는 미디어 타입을 비교해서 첫번째로 찾은 View를 사용한다. 스프링 WebFlux는 HttpMessageWriter로 Json, XML 같은 미디어 타입을 만드는 HttpMessageWriterView를 지원한다. 보통은 WebFlux 설정을 통해 HttpMessageWriterView를 디폴트 뷰로 사용한다. 디폴트 뷰는 요청 미디어 타입과 일치하기만 하면 항상 사용되는 뷰다.
스프링 WebFlux는 어노테이션 기반 프로그램밍 모델을 지원하기 때문에 @Controller, @RestController 컴포넌트로 요청을 매핑하고, 입력을 받고, 예외처리를 할수 있다. 컨트롤러는 메소드를 여러가지로 활용할 수 있어서 클래스를 상속하거나 인터페이스를 구현하지 않아도 된다.
@RestController
public class HelloController {
@GetMapping("/hello")
public String handle() {
return "Hello WebFlux";
}
}
위 코드에서는 response body에 쓸 String을 리턴한다.
컨트롤러는 표준 스프링 빈으로 정의한다. @Controller 어노테이션을 달면 스프링이 클래스패스 내의 다른 @Component 클래스처럼 자동으로 스캔하고 빈으로 등록한다. 이 어노테이션을 선언하면 그 클래스가 Web 컴포넌트라는 뜻이기도 하다. @Controller 빈을 자동으로 등록하려면 아래와 같이 컴포넌트 스캔을 위한 설정이 필요하다.
@Configuration
@ComponentScan("org.example.web")
public class WebConfig {
// ...
}
@RestController는 자체에 @Controller, @ResponseBody를 선언하고 있어, 컨트롤러 내의 모든 메소드에 @ResponseBody를 상속한다. 따라서 리턴값으로 View를 만들지 않고 response body에 바로 쓸 수 있다.
컨트롤러 메소드 요청을 매핑할 때는 @RequestMapping을 사용한다. 이 어노테이션에 있는 속성으로 URL, HTTP 메소드, 요청 파라미터, 헤더, 미디어 타입을 매칭할 수 있다. 메소드에 선언하거나 모든 메소드에서 공유하고 싶을 때 클래스 레벨에 선언한다. @GetMapping, @PostMapping, @PutMapping, @DeleteMapping, @PatchMapping은 HTTP 메소드를 바로 지정할 수 있다. 이 어노테이션은 컨트롤러 메소드에서 거의 대부분이 HTTP 메소드 하나만 담당하는 일종의 커스텀 어노테이션이다. 이 어노테이션을 선언하더라도 다른 매핑 조건을 공통으로 사용하려면 클래스 레벨의 @RequestMapping을 선언해야 한다.
@RequestMapping 핸들러는 다양한 컨트롤러 메소드 인자와 리턴값을 지원하므로 원하는 것을 선택하면 된다. 블로킹 I/O로 받는 인자(예를 들어 response body를 읽는 경우)는 리액티브 유형(Reactor, RxJava 또는 기타)을 사용할 수 있다. 이런 타입은 Description 컬럼에 명시되어 있다. 블로킹 없는 인자는 리액티브 타입을 사용하지 않는다. 일부 어노테이션은(예를 들어 @RequestParam, @RequestHeader 등) required attribute로 필수 여부를 지정할 수 있으며 JDK 8의 java.util.Optional을 사용해도 된다. 효과는 required=false와 동일하다.
@ModelAttribute 어노테이션은 다음과 같이 사용할 수 있다.
여기서는 @ModelAttribute 메소드에 대해 설명한다. 컨트롤러는 @ModelAttribute 메소드를 얼마든지 가질 수 있다. 이러한 모든 메소드는 동일한 컨트롤러에서 @RequestMapping 메소드 앞에 호출된다. @ModelAttribute 메소드는 @ControllerAdvice를 통해 컨트롤러 간에 공유할 수도 있다.
@ModelAttribute 메소드는 여러가지 방법으로 활용할 수 있다. 지원하는 인자는 대부분 @RequestMapping 메소드와 동일하다.(@ModelAttribute 자체와 request body와 관련된 것은 제외) 다음은 @ModelAttribute 메소드 사용 예제이다.
@ModelAttribute
public void populateModel(@RequestParam String number, Model model) {
model.addAttribute(accountRepository.findAccount(number));
// add more ...
}
WebDataBinder는 @Controller나 @ControllerAdvice 클래스에서 @InitBinder 메소드로 초기화할 수 있다. @InitBinder 메소드는 다음과 같이 사용할 수 있다.
@InitBinder 메소드롤 컨트롤러별 java.beans.PropertyEditor나 스프링 Converter, Formatter를 등록할 수 있다. FormattingConversionService에서 전역으로 사용하는 Converter, Formatter는 웹플럭스 설정으로 등록한다. @InitBinder 메소드가 지원하는 인자는 @ModelAttribute(커맨드 객체)만 제외하고 대부분 @RequestMapping 메소드와 동일하다. 보통은 WebDataBinder를 인자로 받아 컴포넌트를 등록하고 void를 리턴한다. 다음은 @InitBinder 어노테이션을 사용하는 예제이다.
@Controller
public class FormController {
@InitBinder
public void initBinder(WebDataBinder binder) {
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
dateFormat.setLenient(false);
binder.registerCustomEditor(Date.class, new CustomDateEditor(dateFormat, false));
}
// ...
}
@Controller와 @ControllerAdvice 클래스 메소드에 @ExceptionHandler를 선언하면 컨트롤러 메서드의 예외를 처리할 수 있다. 다음은 예외를 처리하는 예제이다.
@Controller
public class SimpleController {
// ...
@ExceptionHandler
public ResponseEntity<String> handle(IOException ex) {
// ...
}
}
@ExceptionHandler, @InitBinder, @ModelAttribute 메소드는 @Controller 클래스(혹은 상속한 클래스)에 적용된다. 모든 컨트롤러에 적용하고 싶다면 @ControllerAdvice나 @RestControllerAdvice를 선언한 클래스 안에 만들어야 한다.
@ControllerAdvice는 @Component 어노테이션이 선언되어 있기 때문에 컴포넌트 스캔으로 스프링 빈에 등록할 수 있다. @RestControllerAdvice는 @ControllerAdvice와 @ResponseBody가 둘 다 선언되어 있어 @ExceptionHandler 메소드에서 리턴한 값은 메시지 변환을 통해 view를 만들거나 템플릿을 랜더링하는 대신 response body로 렌더링한다.
애플리케이션을 기동하면 프레임워크 내부에서 @ControllerAdvice를 선언한 스프링 빈을 찾아 @RequestMapping과 @ExceptionHandler 메소드를 적용한다. 전역에 설정한 @ExceptionHandler 메소드는 @Controller 메소드 다음에 적용한다. 반대로 전역 @ModelAttribute, @InitBinder 메소드는 @Controller 메소드 전에 적용한다.
기본적으로 @ControllerAdvice 메소드는 모든 요청에 적용되지만 다음 예제처럼 어노테이션 attribute로 컨트롤러를 지정할 수 있다.
// Target all Controllers annotated with @RestController
@ControllerAdvice(annotations = RestController.class)
public class ExampleAdvice1 {}
// Target all Controllers within specific packages
@ControllerAdvice("org.example.controllers")
public class ExampleAdvice2 {}
// Target all Controllers assignable to specific classes
@ControllerAdvice(assignableTypes = {ControllerInterface.class, AbstractController.class})
public class ExampleAdvice3 {}
스프링 WebFlux는 경량화 된 함수형 프로그래밍 모델을 지원한다. WebFlux.fn이라고 하는 이 모델은 함수로 요청을 라우팅하고 핸들링하기 때문에 불변성(immutability)을 보장한다. 함수형 모델과 어노테이션 모델 중 하나를 선택하면 되는데 둘 다 리액티브 코어를 기반으로 한다.
WebFlux.fn에선 HandlerFunction이 HTTP 요청을 처리한다. HandlerFunction은 ServerRequest를 받아 비동기 ServerResponse(예를 들어 Mono
요청은 RouterFunction이 핸들러 펑션에 라우팅한다. RouterFunction은 ServerRequest를 받아 비동기 HandlerFunction(예를 들어 Mono
라이터를 만들 때는 아래 예제처럼 RouterFunctions.route()가 제공하는 빌더를 사용할 수 있다.
import static org.springframework.http.MediaType.APPLICATION_JSON;
import static org.springframework.web.reactive.function.server.RequestPredicates.*;
import static org.springframework.web.reactive.function.server.RouterFunctions.route;
PersonRepository repository = ...
PersonHandler handler = new PersonHandler(repository);
RouterFunction<ServerResponse> route = route()
.GET("/person/{id}", accept(APPLICATION_JSON), handler::getPerson)
.GET("/person", accept(APPLICATION_JSON), handler::listPeople)
.POST("/person", handler::createPerson)
.build();
public class PersonHandler {
// ...
public Mono<ServerResponse> listPeople(ServerRequest request) {
// ...
}
public Mono<ServerResponse> createPerson(ServerRequest request) {
// ...
}
public Mono<ServerResponse> getPerson(ServerRequest request) {
// ...
}
}
RouterFunction을 실행하는 방법 중 하나는 HttpHandler로 변환해 내장된 서버 어댑터에 등록하는 것이다.
ServerRequest와 ServerResponse는 JDK 8 방식으로 HTTP 요청과 응답에 접근할 수 있는 불변(immutable) 인터페이스이다. 모든 요청, 응답 body 모두 리액티브 스트림 back pressure로 처리한다. request body는 리액터 Flux나 Mono로 표현한다. response body는 Flux와 Mono를 포함한 어떤 리액티브 스트림 Publisher든 상관없다.
ServerRequest로 HTTP 메소드, URI, 헤더, 쿼리 파라미터에 접근할 수 있으며, body를 추출할 수 있는 메소드를 제공한다.
다음은 request body를 Mono
Mono<String> string = request.bodyToMono(String.class);
다음 예제는 body를 Flux
Flux<Person> people = request.bodyToFlux(Person.class);
위 예제에서 사용한 메소드는 함수형 인터페이스 BodyExtractor를 받는 ServerRequest.body(BodyExtractor) 메소드의 축약 버전이다. BodyExtractors 유틸리티 클래스에 있는 인터페이스를 활용해도 된다. 예를 들면 앞의 예제는 아래와 같이 작성할 수도 있다.
Mono<String> string = request.body(BodyExtractors.toMono(String.class));
Flux<Person> people = request.body(BodyExtractors.toFlux(Person.class));
아래 예제는 form 데이터에 접근하는 방법을 보여준다.
Mono<MultiValueMap<String, String>> map = request.formData();
다음은 multipart 데이터를 map으로 가져오는 예제이다.
Mono<MultiValueMap<String, Part>> map = request.multipartData();
다음은 multipart 데이터를 스트리밍 방식으로 한번에 하나씩 가져온다.
Flux<Part> parts = request.body(BodyExtractors.toParts());
HTTP 응답은 ServerResponse로 접근할 수 있으며 이 인터페이션은 불변이기 때문에 build 메소드로 생성한다. 빌더로 헤더를 추가하거나 상태코드, body를 설정할 수 있다. 다음은 JSON 컨텐츠로 200(OK) 응답을 만드는 예제이다.
Mono<Person> person = ...
ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(person, Person.class);
다음 예제는 boby 없이 Location 헤더만 201(CREATED) 응답을 만든다.
URI location = ...
ServerResponse.created(location).build();
hint 파라미터를 넘기면 사용하는 코덱에 따라 body 직렬화/역직렬화 방식을 커스텀 할 수 있다. 아래 예제처럼 Jackson JSON View를 지정할 수 있다.
ServerResponse.ok().hint(Jackson2CodecSupport.JSON_VIEW_HINT, MyJacksonView.class).body(...);
핸들러 평션은 다음처럼 람다로 만들 수 있다.
HandlerFunction<ServerResponse> helloWorld =
request -> ServerResponse.ok().bodyValue("Hello World");
편리한 방식이긴 하지만 펑션을 여러 개 사용해야 한다면 인라인 람다로 만들기는 부담스럽다. 이럴 때는 핸들러 클래스로 관련 핸들러 펑션을 묶을 수 있다. 핸들러 클래스는 어노테이션 기반 애플리케이션의 @Controller와 비슷하다. 다음 예제는 리액티브 Person 레퍼지토리와 관련된 요청을 처리한다.
import static org.springframework.http.MediaType.APPLICATION_JSON;
import static org.springframework.web.reactive.function.server.ServerResponse.ok;
public class PersonHandler {
private final PersonRepository repository;
public PersonHandler(PersonRepository repository) {
this.repository = repository;
}
public Mono<ServerResponse> listPeople(ServerRequest request) {
Flux<Person> people = repository.allPeople();
return ok().contentType(APPLICATION_JSON).body(people, Person.class);
}
public Mono<ServerResponse> createPerson(ServerRequest request) {
Mono<Person> person = request.bodyToMono(Person.class);
return ok().build(repository.savePerson(person));
}
public Mono<ServerResponse> getPerson(ServerRequest request) {
int personId = Integer.valueOf(request.pathVariable("id"));
return repository.getPerson(personId)
.flatMap(person -> ok().contentType(APPLICATION_JSON).bodyValue(person))
.switchIfEmpty(ServerResponse.notFound().build());
}
}
함수형 프로그래밍 모델은 스프링 validation facilities를 사용해서 request body를 검증할 수 있다. 다음 예제는 Person에 대한 커스텀 스프링 Validator 구현체를 보여주고 있다.
public class PersonHandler {
private final Validator validator = new PersonValidator();
// ...
public Mono<ServerResponse> createPerson(ServerRequest request) {
Mono<Person> person = request.bodyToMono(Person.class).doOnNext(this::validate);
return ok().build(repository.savePerson(person));
}
private void validate(Person person) {
Errors errors = new BeanPropertyBindingResult(person, "person");
validator.validate(person, errors);
if (errors.hasErrors()) {
throw new ServerWebInputException(errors.toString());
}
}
}
라우터 펑션은 요청을 그에 맞는 HandlerFunction으로 라우팅한다. 라우팅 펑션을 직접 만들기보단, 보통 RouterFunctions 유틸리티 클래스를 사용한다. RouterFunctions.route()가 리턴하는 빌더를 사용하거나 RouterFunctions.route(RequestPredicate, HandlerFunction)으로 직접 라우터를 만들 수 있다. route() 빌더를 사용하면 static 메소드를 직접 임포트하지 않아도 된다. 예를 들어 GET 요청을 매핑할 수 있는 GET(String, HandlerFunction) 메소드와 POST 요청을 매핑하는 POST(String, HandlerFunction) 메소드가 있다. 빌더는 HTTP 메소드 외에 다른 조건으로 요청을 매핑할 수는 인터페이스도 제공한다. 각 HTTP 메소드는 RequestPredicate 파라미터를 받은 메소드를 오버로딩하고 있기 때문에 다른 조건을 추가할 수 있다.
RequestPredicate를 직접 만들어도 되지만 요청 path, HTTP 메소드, 컨텐츠 타입 등 자주 사용하는 구현체는 RequestPredicates 유틸리티 클래스에 준비되어 있다. 다음은 유틸리티 클래스로 Accept 헤더 조건을 추가하는 예제이다.
RouterFunction<ServerResponse> route = RouterFunctions.route()
.GET("/hello-world", accept(MediaType.TEXT_PLAIN),
request -> ServerResponse.ok().bodyValue("Hello World")).build();
여러 조건을 함께 사용할 수 있다.
라우터 평션은 정해진 순서대로 실행한다. 첫번째 조건과 일치하지 않으면 두번째를 실행하는 식이다. 따라서 구체적인 조건을 앞에 선언해야 한다. 어노테이션 프로그래밍 모델에선 자동으로 가장 구체적인 컨트롤러 메소드를 실행하지만 함수형 모델에서는 그렇지 않다는 점을 유의해야 한다. build()를 호출하면 빌더에 정의한 모든 라우터 펑션을 RouterFunction 한 개로 합친다. 다음 방법으로도 여러 라우터 펑션을 조합할 수 있다.
import static org.springframework.http.MediaType.APPLICATION_JSON;
import static org.springframework.web.reactive.function.server.RequestPredicates.*;
PersonRepository repository = ...
PersonHandler handler = new PersonHandler(repository);
RouterFunction<ServerResponse> otherRoute = ...
RouterFunction<ServerResponse> route = route()
.GET("/person/{id}", accept(APPLICATION_JSON), handler::getPerson)
.GET("/person", accept(APPLICATION_JSON), handler::listPeople)
.POST("/person", handler::createPerson)
.add(otherRoute)
.build();
path가 같으면 대부분 같은 조건으로 사용하므로 라우터 평션을 그룹핑하는 경우가 많다. 앞의 예제는 라우터 펑션 세 개가 /person을 path 조건으로 사용했다. 어노테이션을 사용했다면 클래스 레벨이 @RequestMapping을 선언해 중복 코드를 줄일 수 있다. WebFlux.fn에선 path 메소드로 path 조건을 공유한다. 예를 들어 위 코드는 아래 예제처럼 라우터 펑션을 한번 감싸 개선할 수 있다.
RouterFunction<ServerResponse> route = route()
.path("/person", builder -> builder
.GET("/{id}", accept(APPLICATION_JSON), handler::getPerson)
.GET(accept(APPLICATION_JSON), handler::listPeople)
.POST(handler::createPerson))
.build();
path가 가장 흔하기 하지만 빌더의 nest 메소드는 다른 조건도 감쌀 수 있다. 위 코드는 여전히 Accpet 헤더가 중복이다 nest 메소드를 함께 사용하면 코드를 한층 더 개선할 수 있다.
RouterFunction<ServerResponse> route = route()
.path("/person", b1 -> b1
.nest(accept(APPLICATION_JSON), b2 -> b2
.GET("/{id}", handler::getPerson)
.GET(handler::listPeople))
.POST(handler::createPerson))
.build();
HTTP 서버에선 어떻게 라우터 펑션을 실행할까? 간단하게는 다음과 같이 라우터 펑션을 HttpHandler로 변환할 수 있다.
스프링 부트에서도 사용하는 좀 더 일반적인 옵션은 WebFlux Config로 컴포넌트를 스프링 빈으로 정의하고 DispatcherHandler와 함께 실행하는 것이다. 프레임워크는 다음과 같은 컴포넌트로 함수형 엔드포인트를 지원하는데 웹플럭스 설정을 사용하면 이를 모두 스프링 빈으로 정의한다.
핸들러 펑션에 필터를 적용할 땐 라우터 빌더의 before, after, filter 메소드를 사용한다. 이 기능을 어노테이션 모델로 구현하면 @ControllerAdvice나 ServletFilter를 사용했을 것이다. 필터는 빌더의 모든 라우터 펑션에 적용된다. 이 말은 필터를 감싸져 있는 라우터에서 정의하면 상위 레벨에는 적용되지 않는다는 뜻이다.
RouterFunction<ServerResponse> route = route()
.path("/person", b1 -> b1
.nest(accept(APPLICATION_JSON), b2 -> b2
.GET("/{id}", handler::getPerson)
.GET(handler::listPeople)
.before(request -> ServerRequest.from(request)
.header("X-RequestHeader", "Value")
.build()))
.POST(handler::createPerson))
.after((request, response) -> logResponse(response))
.build();
라우터 빌더의 필터 메서드는 서버 요청과 핸들러 함수를 받아 서버 응답을 반환하는 함수인 핸들러 필터 함수를 받는다. 핸들러 함수 매개변수는 체인의 다음 요소를 나타낸다. 일반적으로 라우팅되는 핸들러이지만 여러 개의 필터가 적용되는 경우 다른 필터가 될 수도 있다. 이제 path를 보고 요청을 허가할지 말지를 결정하는 SecurityManager가 있다고 가정하고 간단한 보안 필터를 라우터에 적용해 보자.
SecurityManager securityManager = ...
RouterFunction<ServerResponse> route = route()
.path("/person", b1 -> b1
.nest(accept(APPLICATION_JSON), b2 -> b2
.GET("/{id}", handler::getPerson)
.GET(handler::listPeople))
.POST(handler::createPerson))
.filter((request, next) -> {
if (securityManager.allowAccessTo(request.path())) {
return next.handle(request);
}
else {
return ServerResponse.status(UNAUTHORIZED).build();
}
})
.build();
위 예제를 보면 next.handle(ServerRequest) 호출은 선택이라는 점을 알 수 있다. 여기선 접근을 허가할 때만 실행했다. 빌더의 filter 메소드 대신 RouterFunction.filter(HandlerFilterFunction)로 필터를 추가하는 방법도 있다.
함수형 엔드포인트에서 CORS는 CorsWebFilter로 지원한다.
스프링 WebFlux는 리액티브, 논블로킹 HTTP 요청을 위한 WebClient를 제공한다. 웹 클라이언트는 리액티브 타입을 사용하는 함수형 API이기 때문에 선언적인(declarative) 프로그래밍이 가능하다. 웹플럭스 클라이언트와 서버는 동일한 논블로킹 코덱으로 요청, 응답을 인코딩, 디코딩한다.
WebClient는 요청을 수행하기 위해 HTTP 클라이언트 라이브러리에 처리를 위임하며 아래와 같은 기능을 기본으로 제공한다.
WebClient는 가장 간단하게는 스태틱 팩토리 메소드로 만들 수 있다.
위 메소드는 디폴트 세팅으로 Reactor Netty HttpClient를 사용하므로 클래스패스에 io.projectreactor.netty:reactor-netty가 있어야 한다.
다른 옵션을 사용하려면 WebClient.builder()를 사용한다.
다음 예제는 HTTP 코덱을 설정한다.
WebClient client = WebClient.builder()
.codecs(configurer -> ... )
.build();
WebClient는 한 번 빌드하고 나면 상태를 변경할 수 없다. 단 다음 예제와 같이 원본 인스턴스는 그대로 두고 복사해와서 설정을 추가할 수 있다.
WebClient client1 = WebClient.builder()
.filter(filterA).filter(filterB).build();
WebClient client2 = client1.mutate()
.filter(filterC).filter(filterD).build();
// client1 has filterA, filterB
// client2 has filterA, filterB, filterC, filterD
스프링 WebFlux는 애플리케이션 메모리 이슈를 방지하기 위해 코덱의 메모리 버퍼 사이즈를 제한한다. 디폴트는 256KB로 설정되어 있는데 버퍼가 부족하면 다음과 같은 에러가 보인다.
org.springframework.core.io.buffer.DataBufferLimitException: Exceeded limit on max bytes to buffer
다음 코드를 사용하면 모든 디폴트 코덱의 최대 버퍼 사이즈를 조절할 수 있다.
WebClient webClient = WebClient.builder()
.codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(2 * 1024 * 1024))
.build();
HttpClient는 Reactor Netty 설정을 커스텀할 수 있는 간단한 설정 프리셋을 가지고 있다.
HttpClient httpClient = HttpClient.create().secure(sslSpec -> ...);
WebClient webClient = WebClient.builder()
.clientConnector(new ReactorClientHttpConnector(httpClient))
.build();
기본적으로 HttpClient는 reactor.netty.http.HttpResources에 묶여 있는 Reactor Netty의 글로벌 리소스를 사용한다. 이는 이벤트 루프 쓰레드와 커넥션 풀도 포함한다.이벤트 루프로 동시성을 제어하려면 공유 리소스를 고정해 놓고 사용하는게 좋기 때문이다. 이 모드에서는 프로세스가 종료될 때까지 공유 자원을 active 상태로 유지한다.서버가 프로세스와 함께 중단된다면 명시적으로 리소스를 종료시킬 필요는 없다. 하지만 프로세스 내에서 서버를 시작하거나 중단할 수 있다면(예를 들어 WAR로 배포한 스프링 MVC 애플리케이션) 다음 예제처럼 스프링이 관리하는 ReactorResourceFactory 빈을 globalResources=true롤 선언해야 스프링 ApplicationContext를 닫을 때 Reactor Netty 글러벌 리소스도 종료한다.
@Bean
public ReactorResourceFactory reactorResourceFactory() {
return new ReactorResourceFactory();
}
원한다면 글로벌 Reactor Netty 리소스를 사용하지 않게 만들수도 있다. 하지만 아래 예제처럼 직접 모든 Reactor Netty 클라이언트와 서버 인스턴스가 공유 자원을 사용하게 만들어야 한다.
@Bean
public ReactorResourceFactory resourceFactory() {
ReactorResourceFactory factory = new ReactorResourceFactory();
factory.setUseGlobalResources(false);
return factory;
}
@Bean
public WebClient webClient() {
Function<HttpClient, HttpClient> mapper = client -> {
// Further customizations...
};
ClientHttpConnector connector =
new ReactorClientHttpConnector(resourceFactory(), mapper);
return WebClient.builder().clientConnector(connector).build();
}
다음은 커넥션 타임아웃을 설정하는 코드다.
import io.netty.channel.ChannelOption;
HttpClient httpClient = HttpClient.create()
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000);
WebClient webClient = WebClient.builder()
.clientConnector(new ReactorClientHttpConnector(httpClient))
.build();
다음은 read/write 타임아웃을 설정하는 코드다.
import io.netty.handler.timeout.ReadTimeoutHandler;
import io.netty.handler.timeout.WriteTimeoutHandler;
HttpClient httpClient = HttpClient.create()
.doOnConnected(conn -> conn
.addHandlerLast(new ReadTimeoutHandler(10))
.addHandlerLast(new WriteTimeoutHandler(10)));
// Create WebClient...
다음은 모든 요청에 대한 타임아웃을 설정하는 코드다.
HttpClient httpClient = HttpClient.create()
.responseTimeout(Duration.ofSeconds(2));
// Create WebClient...
다음은 특정 요청에 타임아웃을 설정하는 코드다.
WebClient.create().get()
.uri("https://example.org/path")
.httpRequest(httpRequest -> {
HttpClientRequest reactorRequest = httpRequest.getNativeRequest();
reactorRequest.responseTimeout(Duration.ofSeconds(2));
})
.retrieve()
.bodyToMono(String.class);
다음은 Jetty HttpClient 설정을 커스텀하는 예제이다.
HttpClient httpClient = new HttpClient();
httpClient.setCookieStore(...);
WebClient webClient = WebClient.builder()
.clientConnector(new JettyClientHttpConnector(httpClient))
.build();
HttpClient는 전용 리소스(Executor. ByteBufferPool, Scheduler)를 생성해서 기본적으로 프로세스가 종료되거나 stop()을 호출할 때까지 유지한다.
다음 예제처럼 스프링이 관리하는 JettyResourceFactory 빈을 정의하면 여러 Jetty 클라이언트(혹은 서버) 인스턴스에서 리소스를 공유할 수 있고 스프링 ApplicationContext를 닫을 때 리소스도 종료시킬 수 있다.
@Bean
public JettyResourceFactory resourceFactory() {
return new JettyResourceFactory();
}
@Bean
public WebClient webClient() {
HttpClient httpClient = new HttpClient();
// Further customizations...
ClientHttpConnector connector =
new JettyClientHttpConnector(httpClient, resourceFactory());
return WebClient.builder().clientConnector(connector).build();
}
다음은 Apache HttpComponents HttpClient 설정을 커스텀하는 예제이다.
HttpAsyncClientBuilder clientBuilder = HttpAsyncClients.custom();
clientBuilder.setDefaultRequestConfig(...);
CloseableHttpAsyncClient client = clientBuilder.build();
ClientHttpConnector connector = new HttpComponentsClientHttpConnector(client);
WebClient webClient = WebClient.builder().clientConnector(connector).build();
retrieve()는 response body를 받아 디코딩하는 간단한 메소드이다. 사용방법은 아래와 같다.
WebClient client = WebClient.create("https://example.org");
Mono<ResponseEntity<Person>> result = client.get()
.uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
.retrieve()
.toEntity(Person.class);
혹은 body만 받아올 수 있다.
WebClient client = WebClient.create("https://example.org");
Mono<Person> result = client.get()
.uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
.retrieve()
.bodyToMono(Person.class);
다음 예제처럼 응답을 객체 스트림으로도 디코딩할 수 있다.
Flux<Quote> result = client.get()
.uri("/quotes").accept(MediaType.TEXT_EVENT_STREAM)
.retrieve()
.bodyToFlux(Quote.class);
4xx 또는 5xx 응답코드를 받으면 디폴트는 WebClientResponseException 또는 각 HTTP 상태에 해당하는 WebClientResponseException.BadRequest, WebClientResponseException.NotFound 등의 하위 exception을 던진다. 다음 예제처럼 onStatus 메소드로 상태별 exception을 커스텀할 수 있다.
Mono<Person> result = client.get()
.uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
.retrieve()
.onStatus(HttpStatus::is4xxClientError, response -> ...)
.onStatus(HttpStatus::is5xxServerError, response -> ...)
.bodyToMono(Person.class);
exchangeToMono() 및 exchangeToFlux() 메서드(또는 Kotlin의 awaitExchange {} 및 exchangeToFlow {})는 응답 상태에 따라 응답을 다르게 디코딩하는 등 더 많은 제어가 필요한 곳에 유용하다.
Mono<Person> entityMono = client.get()
.uri("/persons/1")
.accept(MediaType.APPLICATION_JSON)
.exchangeToMono(response -> {
if (response.statusCode().equals(HttpStatus.OK)) {
return response.bodyToMono(Person.class);
}
else {
// Turn to error
return response.createException().flatMap(Mono::error);
}
});
위의 방법을 사용할 때는 반환된 Mono 또는 Flux가 완료된 후 응답 본문을 확인하고 소비되지 않은 경우 메모리 및 연결 누수를 방지하기 위해 해제한다. 따라서 응답은 더 이상 다운스트림에서 디코딩할 수 없다. 필요한 경우 응답을 디코딩하는 방법을 선언하는 것은 제공된 함수에 달려 있다.
request body는 Mono, 코틀린 코루틴 Deferred 등 ReactiveAdapterRegistry에 등록된 모든 비동기 타입으로 인코딩할 수 있다.
Mono<Person> personMono = ... ;
Mono<Void> result = client.post()
.uri("/persons/{id}", id)
.contentType(MediaType.APPLICATION_JSON)
.body(personMono, Person.class)
.retrieve()
.bodyToMono(Void.class);
다음 예제처럼 객체 스트림으로도 인코딩할 수 있다.
Flux<Person> personFlux = ... ;
Mono<Void> result = client.post()
.uri("/persons/{id}", id)
.contentType(MediaType.APPLICATION_STREAM_JSON)
.body(personFlux, Person.class)
.retrieve()
.bodyToMono(Void.class);
비동기 타입이 아닌 실제 값을 가지고 있다면 bodyValue를 사용한다.
Person person = ... ;
Mono<Void> result = client.post()
.uri("/persons/{id}", id)
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(person)
.retrieve()
.bodyToMono(Void.class);
form 데이터를 보내려면 MultiValueMap<String, String>을 body로 사용해야 한다. 이 때는 FormHttpMessageWriter가 자동으로 content-type을 application/x-www-form-urlencoded로 설정한다. 다음은 MultiValueMap<String, String>을 사용하는 예제이다.
MultiValueMap<String, String> formData = ... ;
Mono<Void> result = client.post()
.uri("/path", id)
.bodyValue(formData)
.retrieve()
.bodyToMono(Void.class);
BodyInserters를 사용하면 인라인으로 form 데이터를 만들 수 있다.
import static org.springframework.web.reactive.function.BodyInserters.*;
Mono<Void> result = client.post()
.uri("/path", id)
.body(fromFormData("k1", "v1").with("k2", "v2"))
.retrieve()
.bodyToMono(Void.class);
multipart 데이터를 보낼 때는 MultiValueMap<String, ?>을 사용해서 각 value에 part 컨텐츠를 나타내는 Object 인스턴스나 part의 컨텐츠와 헤더를 나타내는 HttpEntity를 담아야 한다. MultipartBodyBuilder를 사용하면 편리하다.
MultipartBodyBuilder builder = new MultipartBodyBuilder();
builder.part("fieldPart", "fieldValue");
builder.part("filePart1", new FileSystemResource("...logo.png"));
builder.part("jsonPart", new Person("Jason"));
builder.part("myPart", part); // Part from a server request
MultiValueMap<String, HttpEntity<?>> parts = builder.build();
일반적으로는 part별로 content-type을 명시하지 않아도 된다. Content Type은 직렬화할때 쓰는 HttpMessageWriter나 Resource의 경우 파일 확장자에 따라 자동으로 결정한다. 필요하다면 빌더 part 메소드 중 MediaType을 받는 메소드를 사용하면 된다. MultiValueMap을 만들었다면 가장 간단하게는 다음 예제처럼 body 메소드로 WebClient에 넘길 수 있다.
MultipartBodyBuilder builder = ...;
Mono<Void> result = client.post()
.uri("/path", id)
.body(builder.build())
.retrieve()
.bodyToMono(Void.class);
MultiValueMap에 전형적인 form 데이터(application/x-www-form-urlencoded) 등 String이 아닌 갓이 하나라도 들어있으면 content-type을 multipart/form-data로 설정하지 않아도 된다. MultipartBodyBuilder를 사용하면 항상 HttpEntity로 감싸주면 된다. MultipartBodyBuilder 대신 BodyInserters를 사용하면 인라인으로 multipart 컨텐츠를 만들 수 있다.
import static org.springframework.web.reactive.function.BodyInserters.*;
Mono<Void> result = client.post()
.uri("/path", id)
.body(fromMultipartData("fieldPart", "value").with("filePart", resource))
.retrieve()
.bodyToMono(Void.class);
다음 예제와 같이 WebClient.Builder를 통해 클라이언트 필터(ExchangeFilterFunction)를 등록하여 요청을 가로채고 수정할 수 있다.
WebClient client = WebClient.builder()
.filter((request, next) -> {
ClientRequest filtered = ClientRequest.from(request)
.header("foo", "bar")
.build();
return next.exchange(filtered);
})
.build();
필터는 인증과 같은 교차 문제에 사용할 수 있다. 다음 예제에서는 정적 팩토리 메서드를 통한 기본 인증에 필터를 사용한다.
import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.basicAuthentication;
WebClient client = WebClient.builder()
.filter(basicAuthentication("user", "password"))
.build();
필터는 기존 웹클라이언트 인스턴스를 변경하여 추가하거나 제거할 수 있으므로 원래 웹클라이언트에 영향을 주지 않는 새 웹클라이언트 인스턴스를 생성할 수 있다.
import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.basicAuthentication;
WebClient client = webClient.mutate()
.filters(filterList -> {
filterList.add(0, basicAuthentication("user", "password"));
})
.build();
WebClient는 일련의 필터 체인을 둘러싸고 있는 얇은 외피에 ExchangeFunction이 뒤따른다. 요청을 하고 상위 수준 객체와 인코딩하기 위한 워크플로우를 제공하며 응답 콘텐츠가 항상 소비되도록 하는 데 도움이 된다.필터가 어떤 방식으로든 응답을 처리할 때는 항상 콘텐츠를 소비하거나 웹클라이언트로 다운스트림으로 전파하여 동일한 콘텐츠를 보장할 수 있도록 각별한 주의를 기울여야 한다.다음 예제는 미승인 상태 코드를 처리하지만 예상 여부에 관계없이 모든 응답 콘텐츠가 공개되도록 하는 필터다.
public ExchangeFilterFunction renewTokenFilter() {
return (request, next) -> next.exchange(request).flatMap(response -> {
if (response.statusCode().value() == HttpStatus.UNAUTHORIZED.value()) {
return response.releaseBody()
.then(renewToken())
.flatMap(token -> {
ClientRequest newRequest = ClientRequest.from(request).build();
return next.exchange(newRequest);
});
} else {
return Mono.just(response);
}
});
}
요청에 속성을 추가할 수 있다. 이 기능은 필터 체인을 통해 정보를 전달하고 특정 요청에 대한 필터의 동작에 영향을 주고자 할 때 편리하다.
WebClient client = WebClient.builder()
.filter((request, next) -> {
Optional<Object> usr = request.attribute("myAttribute");
// ...
})
.build();
client.get().uri("https://example.org/")
.attribute("myAttribute", "...")
.retrieve()
.bodyToMono(Void.class);
}
모든 요청에 속성을 삽입할 수 있는 WebClient.Builder 수준에서 전역적으로 defaultRequest 콜백을 구성할 수 있으며, 이는 예를 들어 Spring MVC 애플리케이션에서 ThreadLocal 데이터를 기반으로 요청 속성을 채우는 데 사용될 수 있다.
속성은 필터 체인에 정보를 전달하는 편리한 방법을 제공하지만 현재 요청에만 영향을 준다. 중첩된 추가 요청에 전파되는 정보를 전달하려면(예를 들어 flatMap을 통해), 또는 이후에 실행되는 요청에 전파되는 정보를 전달하려면(예를 들어 concatMap을 통해) Reactor Context를 사용해야 한다.
다음 예제는 리액터 컨텍스트를 리액티브 체인의 끝에 채워 모든 작업에 적용하는 것을 보여준다.
WebClient client = WebClient.builder()
.filter((request, next) ->
Mono.deferContextual(contextView -> {
String value = contextView.get("foo");
// ...
}))
.build();
client.get().uri("https://example.org/")
.retrieve()
.bodyToMono(String.class)
.flatMap(body -> {
// perform nested request (context propagates automatically)...
})
.contextWrite(context -> context.put("foo", ...));
WebClient는 마지막에 결과를 블로킹하면 동기(synchronous)로 결과를 가져온다.
Person person = client.get().uri("/person/{id}", i).retrieve()
.bodyToMono(Person.class)
.block();
List<Person> persons = client.get().uri("/persons").retrieve()
.bodyToFlux(Person.class)
.collectList()
.block();
하지만 API를 여러번 호출하면 각 응답을 따로 블로킹하기보다는 전체 결과를 합쳐서 기다리는게 더 효율적이다.
Mono<Person> personMono = client.get().uri("/person/{id}", personId)
.retrieve().bodyToMono(Person.class);
Mono<List<Hobby>> hobbiesMono = client.get().uri("/person/{id}/hobbies", personId)
.retrieve().bodyToFlux(Hobby.class).collectList();
Map<String, Object> data = Mono.zip(personMono, hobbiesMono, (person, hobbies) -> {
Map<String, String> map = new LinkedHashMap<>();
map.put("person", person);
map.put("hobbies", hobbies);
return map;
})
.block();
위 코드는 단지 한 가지 예시일 뿐이다. 요청이 끝날때까지 블로킹 하지 않고 리액티브 파이프라인을 구축해서 상호독립적으로 원격 호출을 여러번 실행하는 다른 패턴과 연산자도 많다.
스프링 MVC나 WebFlux 컨트롤러에서 Flux나 Mono를 사용한다면 블로킹할 필요가 없다. 단순히 컨트롤러 메소드에서 리액티브 타입을 리턴하기만 하면 된다. 코틀린 코루틴과 스프링 WebFlux에서도 마찬가지다. 컨트롤러 메소드에서 suspend 함수를 사용하거라 Flow를 리턴하면 된다.
참고자료
일반적으로 Ajax 기능은 javascript 언어로 개발하나, server-side 구현에 익숙한 J2EE 개발자들에게는 쉽지 않은 작업이 될 수 있다. Ajax 지원 서비스에서는 Ajax를 이용해 자주 사용되는 기능을 custom tag형태로 제공한다. 기능은 오픈소스 라이브러리인 AjaxTags를 이용한다.
시스템 환경 및 필요 라이브러리
설치 순서
<servlet>
<servlet-name>sourceloader</servlet-name>
<servlet-class>net.sourceforge.ajaxtags.servlets.SourceLoader</servlet-class>
<init-param>
<param-name>prefix</param-name>
<param-value>/ajaxtags</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>sourceloader</servlet-name>
<url-pattern>/ajaxtags/js/*</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>sourceloader</servlet-name>
<url-pattern>/ajaxtags/img/*</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>sourceloader</servlet-name>
<url-pattern>/ajaxtags/css/*</url-pattern>
</servlet-mapping>
ajax:autocomplete
자동완성기능. 보통 검색 입력창에 prefix 문자를 입력하면 해당 추천 검색어를 보여주는 방식으로 이용.
| 파라미터 | 설명 | 필수여부 |
|---|---|---|
| baseUrl | 자동완성기능을 위한 결과 데이터를 보내주는 server-side 액션을 위한 URL. | yes |
| source | 추천 검색어 리스트를 보여줄 텍스트 필드 이름. 입력 필드에 추천 검색리스트를 보여준다면 target과 source를 동일하게 입력한다. | yes |
| target | 사용자가 입력하는 텍스트 필드 이름. | yes |
| parameters | baseUrl에 추가할 파라미터들.여러개일 경우 comma로 구별한다. | yes |
| className | 추천 검색리스트에 적용할 CSS 클래스이름 | yes |
| indicator | Ajax 요청중일때 보여줄 표시. | no |
| minimumCharacters | Ajax 요청을 위한 최소 입력값. | no |
| preFunction | Ajax 요청이 시작되기 전에 동작하는 function 이름. | no |
| postFunction | Ajax 요청이 완료된 후에 동작하는 function 이름. | no |
| errorFunction | Ajax 요청 error시에 동작하는 function 이름. | no |
ajax:select
하나의 셀렉트박스에서 값을 변경하면 다른 셀렉트박스에 연관된 값으로 리스트를 구성. Linked SelectBox.
| 파라미터 | 설명 | 필수여부 |
|---|---|---|
| baseUrl | 자동완성기능을 위한 결과 데이터를 보내주는 server-side 액션을 위한 URL. | yes |
| source | 추천 검색어 리스트를 보여줄 텍스트 필드 이름. 입력 필드에 추천 검색리스트를 보여준다면 target과 source를 동일하게 입력한다. | yes |
| target | 사용자가 입력하는 텍스트 필드 이름. | yes |
| parameters | baseUrl에 추가할 파라미터들.여러개일 경우 comma로 구별한다. | no |
| eventType | no | |
| executeOnLoad | 응답 데이터로 select box를 구성하는 중일때 구성중인지를 별도 표시를 할지 여부.[default=false] | no |
| defaultOptions | Ajax 응답값이 없을때 보여줄 기본 리스트. comma로 구별하여 작성한다. | no |
| preFunction | Ajax 요청이 시작되기 전에 동작하는 function 이름. | no |
| postFunction | Ajax 요청이 완료된 후에 동작하는 function 이름. | no |
| errorFunction | Ajax 요청 error시에 동작하는 function 이름. | no |
| parser | 응답 데이터에 대한 parser.[default=ResponseHtmlParser] | no |
ajax:tabPanel
탭으로 구성된 페이지들 새로 고침 없이 보여 줄때.
| 파라미터 | 설명 | 필수여부 |
|---|---|---|
| id | tabPanel의 ID | yes |
| preFunction | Ajax 요청이 시작되기 전에 동작하는 function 이름. | no |
| postFunction | Ajax 요청이 완료된 후에 동작하는 function 이름. | no |
| errorFunction | Ajax 요청 error시에 동작하는 function 이름. | no |
| parser | 응답 데이터에 대한 parser.[default=ResponseHtmlParser] | no |
others
이외에도 여러 기능이 있다. AjaxTags의 Tag 레퍼런스 및 사용법은 아래 AjaxTags 사이트에서 확인할 수 있다.
AjaxTags의 어떤 태그를 사용하던지, 아래의 작업은 공통적으로 발생한다.
태그 라이브러리 선언
<%@ taglib prefix="ajax" ri="http://ajaxtags.sourceforge.net/tags/ajaxtags" %>
Javascript, CSS 선언
<script type="text/javascript" src="<%=request.getContextPath()%>/ajaxtags/js/prototype.js"></script>
<script type="text/javascript" src="<%=request.getContextPath()%>/ajaxtags/js/scriptaculous/scriptaculous.js"></script>
<script type="text/javascript" src="<%=request.getContextPath()%>/ajaxtags/js/overlibmws/overlibmws.js"></script>
<script type="text/javascript" src="<%=request.getContextPath()%>/ajaxtags/js/ajaxtags.js"></script>
<link type="text/css" rel="stylesheet" href="<%=request.getContextPath()%>/ajaxtags/css/ajaxtags.css" />
<link type="text/css" rel="stylesheet" href="<%=request.getContextPath()%>/ajaxtags/css/displaytag.css" />
AjaxTags를 사용하기 위해서는 결과 데이터가 AjaxTags에서 데이터 형식(XML style)을 갖추어야 한다. 이를 위해 AjaxTags는 AjaxXmlBuilder라는 데이터 가공을 위한 API를 제공한다. 결과 데이터를 AjaxXmlBuilder를 이용해서 변환하는 작업을 View에서 할 수도 있지만, View의 갯수가 기능 단위로 추가될 수도 있으므로, Controller에서 변환한 후에 Model 객체에 담아서 View로 보내고 View는 공통으로 하나를 사용하기를 권한다.
org.ajaxtags.helpers.AjaxXmlBuilder
ajaxXml model에 추가하기
List<Department> deptList = departmentService.getDepartmentList(param);
AjaxXmlBuilder ajaxXmlBuilder = new AjaxXmlBuilder();
for (Iterator iter = deptList.iterator(); iter.hasNext();) {
Department dept = (Department) iter.next();
ajaxXmlBuilder.addItem(dept.getDeptname(), dept.getDeptid());
}
model.addObject("ajaxXml",ajaxXmlBuilder.toString());
결과 데이터는 다음과 같다.
<?xml version="1.0" encoding="UTF-8"?>
<ajax-response>
<response>
<item>
<name>점심메뉴기획팀</name>
<value>1200</value>
</item>
<item>
<name>야근금지역량팀</name>
<value>1300</value>
</item>
...
</response>
</ajax-response>
JSP 페이지에 프린트되는 일반적인 응답방식이 아니므로, 응답 처리를 위한 공통 View를 만들어야 한다. 결과데이터의 형식(XML)을 응답 객체에 설정한다. Controller에서 보낸 Model객체의 결과데이터를 꺼내 write한다.
package com.easycompany.view;
import java.io.PrintWriter;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.view.AbstractView;
public class AjaxXmlView extends AbstractView {
@Override
protected void renderMergedOutputModel(Map model,
HttpServletRequest request, HttpServletResponse response)
throws Exception {
response.setContentType("text/xml");
response.setHeader("Cache-Control", "no-cache");
response.setCharacterEncoding("UTF-8");
PrintWriter writer = response.getWriter();
writer.write((String) model.get("ajaxXml")); //Model Attribute 이름은 공통으로 사용하는 것으로...
writer.close();
}
}
사원 정보 조회 페이지에서, 조회 조건중에 하나인 이름 필드에 자동완성기능(autocomplete)을 적용해 보자. 검색하려는 이름을 입력하기 시작하면, 입력값에 해당하는 prefix를 가진 이름들이 추천 리스트로 나온다.

JSP
/easycompany/webapp/WEB-INF/jsp/employeelist.jsp
...
<%@ taglib prefix="ajax" uri="http://ajaxtags.sourceforge.net/tags/ajaxtags" %>
...
<!--Ajax Tags-->
<script type="text/javascript" src="<%=request.getContextPath()%>/ajaxtags/js/prototype.js"></script>
<script type="text/javascript" src="<%=request.getContextPath()%>/ajaxtags/js/scriptaculous/scriptaculous.js"></script>
<script type="text/javascript" src="<%=request.getContextPath()%>/ajaxtags/js/overlibmws/overlibmws.js"></script>
<script type="text/javascript" src="<%=request.getContextPath()%>/ajaxtags/js/ajaxtags.js"></script>
<link type="text/css" rel="stylesheet" href="<%=request.getContextPath()%>/ajaxtags/css/ajaxtags.css" />
<link type="text/css" rel="stylesheet" href="<%=request.getContextPath()%>/ajaxtags/css/displaytag.css" />
...
<form:form commandName="searchCriteria" action="/easycompany/employeeList.do">
<table width="50%" border="1">
<tr>
<td>사원번호 : <form:input path="searchEid"/> </td>
<td>부서번호 : <form:input path="searchDid"/> </td>
<td>이름 : <form:input path="searchName"/>
</td>
<td><input type="submit" value="검색" onclick="this.disabled=true,this.form.submit();" /></td>
</tr>
</table>
</form:form>
<ajax:autocomplete
baseUrl="${pageContext.request.contextPath}/suggestName.do"
source="searchName"
target="searchName"
className="autocomplete"
minimumCharacters="1" />
...
Controller
com.easycompany.controller.annotation.AjaxController
package com.easycompany.controller.annotation;
...
import net.sourceforge.ajaxtags.xml.AjaxXmlBuilder;
import com.easycompany.view.AjaxXmlView;
@Controller
public class AjaxController {
@Autowired
private EmployeeService employeeService;
@Autowired
private DepartmentService departmentService;
@RequestMapping("/suggestName.do")
protected ModelAndView suggestName(@RequestParam("searchName") String searchName){
ModelAndView model = new ModelAndView(new AjaxXmlView());
List<String> nameList = employeeService.getNameListForSuggest(searchName);
AjaxXmlBuilder ajaxXmlBuilder = new AjaxXmlBuilder();
for(String name:nameList){
ajaxXmlBuilder.addItem(name, name, false);
}
model.addObject("ajaxXml",ajaxXmlBuilder.toString());
return model;
}
}
한글처리설정
위 예제에서 보면 '/suggestName.do?searchName=김' 같이, 사원 이름 prefix값이 파라미터로 전달되는데,
파라미터 값이 한글인 경우 제대로 처리되기 위해서는, {Tomcat DIR}/conf/server.xml에 인코딩 처리를 해줘야 한다.
UTF-8 인코딩을 한다면, <Connector/> 태그에 URIEncoding=“utf-8”을 추가하면 된다.
...
<Connector port="8080" maxHttpHeaderSize="8192"
maxThreads="150" minSpareThreads="25" maxSpareThreads="75"
enableLookups="false" redirectPort="8443" acceptCount="100"
connectionTimeout="20000" disableUploadTimeout="true" URIEncoding="utf-8"/>
...
<Connector port="8009"
enableLookups="false" redirectPort="8443" protocol="AJP/1.3" URIEncoding="utf-8" />
사원 정보 수정(입력) 페이지에서, 상위 부서 정보 select box에서 한 부서를 선택하면, 하위 부서 정보 select box는 해당 상위 부서에 속한 하위 부서 정보들로 옵션을 구성한다.

JSP
/easycompany/webapp/WEB-INF/jsp/addemployee.jsp, /easycompany/webapp/WEB-INF/jsp/modifyemployee.jsp
...
<%@ taglib prefix="ajax" uri="http://ajaxtags.sourceforge.net/tags/ajaxtags" %>
...
<!--Ajax Tags-->
<script type="text/javascript" src="<%=request.getContextPath()%>/ajaxtags/js/prototype.js"></script>
<script type="text/javascript" src="<%=request.getContextPath()%>/ajaxtags/js/scriptaculous/scriptaculous.js"></script>
<script type="text/javascript" src="<%=request.getContextPath()%>/ajaxtags/js/overlibmws/overlibmws.js"></script>
<script type="text/javascript" src="<%=request.getContextPath()%>/ajaxtags/js/ajaxtags.js"></script>
<link type="text/css" rel="stylesheet" href="<%=request.getContextPath()%>/ajaxtags/css/ajaxtags.css" />
<link type="text/css" rel="stylesheet" href="<%=request.getContextPath()%>/ajaxtags/css/displaytag.css" />
...
<form:form commandName="employee">
...
<tr>
<th>부서번호</th>
<td>
<form:select path="superdeptid">
<option value="">상위부서를 선택하세요.</option>
<form:options items="${deptInfoOneDepthCategory}" />
</form:select>
<form:select path="departmentid">
<option value="">근무부서를 선택하세요.</option>
<form:options items="${deptInfoTwoDepthCategory}" />
</form:select>
</td>
</tr>
...
</form:form>
<ajax:select
baseUrl="${pageContext.request.contextPath}/autoSelectDept.do"
parameters="depth=2,superdeptid={superdeptid}"
source="superdeptid"
target="departmentid"
emptyOptionName="Select model"/>
...
부서정보 페이지에서 각 상위부서에 속한 하위부서리스트를 보여줄때, tab으로 처리해서 보여준다.

JSP
/easycompany/webapp/WEB-INF/jsp/departmentlist.jsp
...
<%@ taglib prefix="ajax" uri="http://ajaxtags.sourceforge.net/tags/ajaxtags" %>
...
<!--Ajax Tags-->
<script type="text/javascript" src="<%=request.getContextPath()%>/ajaxtags/js/prototype.js"></script>
<script type="text/javascript" src="<%=request.getContextPath()%>/ajaxtags/js/scriptaculous/scriptaculous.js"></script>
<script type="text/javascript" src="<%=request.getContextPath()%>/ajaxtags/js/overlibmws/overlibmws.js"></script>
<script type="text/javascript" src="<%=request.getContextPath()%>/ajaxtags/js/ajaxtags.js"></script>
<link type="text/css" rel="stylesheet" href="<%=request.getContextPath()%>/ajaxtags/css/ajaxtags.css" />
<link type="text/css" rel="stylesheet" href="<%=request.getContextPath()%>/ajaxtags/css/displaytag.css" />
...
<ajax:tabPanel id="departmentTab">
<c:forEach items="${departmentlist}" var="departmentinfo" varStatus="status">
<c:choose>
<c:when test="${status.first}">
<ajax:tab caption="${departmentinfo.deptname}"
baseUrl="/easycompany/subDepartmentList.do?superdeptid=${departmentinfo.deptid}&depth=2"
defaultTab="true"/>
</c:when>
<c:otherwise>
<ajax:tab caption="${departmentinfo.deptname}"
baseUrl="/easycompany/subDepartmentList.do?superdeptid=${departmentinfo.deptid}&depth=2"/>
</c:otherwise>
</c:choose>
</c:forEach>
</ajax:tabPanel>
...
<spring:message> 태그로 해당 언어에 맞는 메시지를 표시할 수 있다.전자정부 표준 프레임워크에서는 Spring MVC 에서 제공하는 LocaleResolver를 이용한다. 우리는 여기서 LocaleResolver를 알아보고 적용하는 설정과 다국어가 적용된 message resource 를 가져와 활용하는 것을 보도록 하겠다. Spring MVC 는 다국어를 지원하기 위하여 아래와 같은 종류의 LocaleResolver 를 제공하고 있다.
Bean 설정 파일에 정의하지 않을 경우 AcceptHeaderLocaleResolver 를 default 로 적용된다.
CookieLocaleResolver 를 설정하는 경우 사용자의 쿠키에 설정된 Locale 을 읽어 들인다. samlple-servlet.xml
...
<bean id="localeResolver"
class="org.springframework.web.servlet.i18n.CookieLocaleResolver" >
<property name="cookieName" value="clientlanguage"/>
<property name="cookieMaxAge" value="100000"/>
<property name="cookiePath" value="web/cookie"/>
</bean>
...
다음과 같은 속성을 사용할 수 있다.
| 속성 | 기본값 | 설명 |
|---|---|---|
| cookieName | classname + locale | 쿠키 명 |
| cookieMaxAge | integer.MAX_INT | -1 로 해두면 브라우저를 닫을 때 없어짐 |
| cookiepath | / | Path 를 지정하면 해당하는 Path와 그 하위 Path 에서만 참조 |
requst가 가지고 있는 session으로 부터 locale 정보를 가져온다. samlple-servlet.xml
...
<bean id="localeResolver" class="org.springframework.web.servlet.i18n.SessionLocaleResolver" />
...
사용자의 브라우저에서 보내진 request 의 헤더에 accept-language 부분에서 Locale 을 읽어 들인다. 사용자의 브라우저의 Locale 을 나타낸다. samlple-servlet.xml
<bean id="localeResolver" class="org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver" />
Web 을 통해 들어오는 요청을 Charset UTF-8 적용한다.
CharacterEncodingFilter 을 이용하여 encoding 할 수 이도록 아래와 같이 세팅한다.
web.xml
...
<filter>
<filter-name>encoding-filter</filter-name>
<filter-class>
org.springframework.web.filter.CharacterEncodingFilter
</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>encoding-filter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
...
사용자의 브라우저의 Locale 정보를 이용하지 않고 사용자가 선택하여 언어를 직접 선택할 수 있도록 구현하려 한다면 CookieLocaleResolver 나 SessionLocaleResolver 를 이용한다. 먼저 다국어를 지원해야 하므로 메세지를 MessageSource 로 추출하여 구현해야 한다. messageSource는 아래와 같이 설정하였다.
samlple-servlet.xml
context-common.xml 등에 설정된 경우 설정할 필요는 없다.
<bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource">
<property name="basenames">
<list>
<value>classpath:/message/message</value>
</list>
</property>
</bean>
message properties 파일은 아래와 같다. locale에 따라 ko, en 으로 구분하였다. message_ko.properties
view.category=카테고리
message_en.properties
view.category=category
ResourceBundleMessageSource 는 beannames 명으로 messages 을 받아오는데 디폴트의 경우는 messages.properties 에서 message를 받아오고 locale 이 한국어일 경우는 messages_ko_KRproperties 에서 받아오고 영어일 경우는 messages_en_US.properties 에서 받아온다. 아래와 같이 localeResover 과 localeChangeInterceptor 를 등록하고 Annotation 기반에서 동작할 수 있도록 DefaultAnnotationHandlerMapping 에 interceptor 로 등록을 해준다.
samlple-servlet.xml (Spring 3.0이하)
<!-- 세션을 이용한 Locale 이용시-->
<bean id="localeResolver" class="org.springframework.web.servlet.i18n.SessionLocaleResolver"/>
<!-- 쿠키를 이용한 Locale 이용시
<bean id="localeResolver" class="org.springframework.web.servlet.i18n.CookieLocaleResolver"/>
-->
<bean id="localeChangeInterceptor" class="org.springframework.web.servlet.i18n.LocaleChangeInterceptor">
<property name="paramName" value="language"/>
</bean>
<bean id="annotationMapper" class="org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping">
<property name="interceptors">
<list>
<ref bean="localeChangeInterceptor"/>
</list>
</property>
</bean>
samlple-servlet.xml (Spring 3.1이상, 실행환경 대부분 버전이 해당된다.)
<!-- 세션을 이용한 Locale 이용시-->
<bean id="localeResolver" class="org.springframework.web.servlet.i18n.SessionLocaleResolver"/>
<!-- 쿠키를 이용한 Locale 이용시
<bean id="localeResolver" class="org.springframework.web.servlet.i18n.CookieLocaleResolver"/>
-->
<bean id="localeChangeInterceptor" class="org.springframework.web.servlet.i18n.LocaleChangeInterceptor">
<property name="paramName" value="language"/>
</bean>
<bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter">
<property name="webBindingInitializer">
<bean class="egovframework.com.cmm.web.EgovBindingInitializer"/>
</property>
</bean>
<bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping">
<property name="interceptors">
<list>
<ref bean="localeChangeInterceptor" />
</list>
</property>
</bean>
설정 주의사항
<mvc:annotation-driven/>
위의 내용이 설정되어 있을 경우 상단의 RequestMappingHandlerAdapter, RequestMappingHandlerMapping 설정이 적용되지 않는다. mvc:annotation-driven이 기본 설정으로 위 bean을 생성하므로 servlet xml에 선언되어 있을 경우 주석 처리한다. 세부 내용은 Spring MVC Tag Configuration 을 참고한다.
mvc:annotation-driven을 사용하려면 인터셉터를 명시적으로 선언해야 한다. RequestMappingHandlerAdapter, RequestMappingHandlerMapping Bean의 설정 내용을 삭제하고, 아래의 코드를 추가한다.
<mvc:interceptors>
<mvc:interceptor>
<mvc:mapping path="/**" />
<bean id="localeChangeInterceptor" class="org.springframework.web.servlet.i18n.LocaleChangeInterceptor">
<property name="paramName" value="language" />
</bean>
</mvc:interceptor>
</mvc:interceptors>
SessionLocaleResolver 를 이용하여 위와 같이 하였을 경우 Locale 결정은 language 로 Request Parameter 로 넘기게 된다. 카테고리 용어가 영어와 한글로 바뀌는 것을 아래와 같이 볼 수 있다.
리스트를 보여주는 화면에 예를 보자면 Spring 메시지 태그를 이용하여
<spring:message code="view.category" />
으로 표현한다.
<%@ taglib prefix="spring" uri=http://www.springframework.org/tags %>
<form:form commandName="message" >
....
<table border="1" cellspacing="0" class="boardList" summary="List of Category">
<thead>
<tr>
<th scope="col">No.</th>
<th scope="col">
<input name="checkAll" type="checkbox" class="inputCheck" title="Check All" onclick="javascript:fncCheckAll();"/>
</th>
<th scope="col"><spring:message code="view.category" /> ID</th>
<th scope="col"><spring:message code="view.category" /> 명</th>
<th scope="col">사용여부</th>
<th scope="col">Description</th>
<th scope="col">등록자</th>
</tr>
</thead>
....
화면상으로 해당 페이지를 실행해보면 아래와 같다.
한글인 경우 :
http://localhost:8080/sample-web/sale/listCategory.do?language=ko

영어인 경우 :
http://localhost:8080/sample-web/sale/listCategory.do?language=en

Java 소스내에서 locale 적용 메시지 가져오기
참고로 MessageSource 는 아래와 같은 메소드로 이루어져 있다.(실제로 여기서의 구현체는 ResourceBundleMessageSource 임.)

String msg = messageSource.getMessage(messageKey, messageParameters, defaultMessage, locale);
인증, 권한 같은 일반적인(통상적인) 개념의 Security 서비스는 Spring Security를 활용한 공통기반 레이어에서 제공한다. 화면 처리 레이어의 Security 서비스는 입력값 유효성 검증 기능을 제공한다. 입력값 유효성 검증(validation)을 위한 기능은 Valang, Jakarta Commons, Spring 등에서 제공하는데, 여기서는 기반 오픈소스로 Jakarta Commons Validator를 선택했다. MVC 프레임워크인 Spring MVC와 Jakarta Commons Validator의 연계와 활용방안을 설명한다.
Jakarta Commons Validator는 필수값, 각종 primitive type(int,long,float…), 최대-최소길이, 이메일, 신용카드번호등의 값 체크등을 할 수 있도록 Template이 제공된다. 또한 client-side, server-side의 검증을 함께 할 수 있으며, Configuration과 에러메시지를 client-side, server-side 별로 따로 하지 않고 한곳에 같이 쓰는 관리상의 장점이 있다. 자세한 설명은 아래의 문서를 참조하라.
입력값 검증을 위한 Validation 기능은 Valang, Jakarta Commons, Spring 등에서 제공한다.
여기서는 Jakarta Commons Validator를 Spring Framework과 연동하여 사용하는 방법에 대해서 설명하고자 한다.
Jakarta Commons Validator는 필수값, 각종 primitive type(int,long,float…), 최대-최소길이, 이메일, 신용카드번호등의 값 체크등을 할 수 있도록 Template이 제공된다.
이 Template은 Java 뿐 아니라 Javascript로도 제공되어 client-side, server-side의 검증을 함께 할 수 있으며,
Configuration과 에러메시지를 client-side, server-side 별로 따로 하지 않고 한곳에 같이 쓰는 관리상의 장점이 있다.
Struts에서는 Commons Validator를 사용하기 위한 org.apache.struts.validator.ValidatorPlugIn 같은 플러그인 클래스를 제공하는데,
Spring에서는 Spring Modules 프로젝트에서 연계 모듈을 제공한다.
여기서는 server-side, client-side validation을 위해,
설치방법, Spring Module에서 제공하는 핵심 클래스인 DefaultValidatorFactory, DefaultValidator와 설정파일인 validator-rules.xml, validator.xml 에 대한 간략한 설명과
예제 프로젝트인 easycompany에 적용하는 과정을 설명한다.
Spring Validator와 Commons Validator의 연계를 위해 중요한 역할을 하는 클래스인 DefaultValidatorFactory, DefaultBeanValidator에 대해 간략하게 설명하자면 아래 표와 같다.
| DefaultValidatorFactory | DefaultBeanValidator |
|---|---|
| 프로퍼티 ‘validationConfigLocationsApache’에 정의된 Validation rule을 기반으로 Commons Validator들의 인스턴스를 얻는다. | DefaultBeanValidator는 org.springframework.validation.Validator를 implements하고 있지만, DefaultValidatorFactory가 가져온 Commons Validator의 인스턴스를 이용해 validation을 수행한다. Controller에 validation 수행할때 이 DefaultBeanValidator를 참조하면 된다. |
아래 코드를 참조해 빈 정의 파일(예제에는 easycompany-servlet.xml)에 다음과 같이 ValidatorFactory,Validator,validator-rules.xml,validation.xml 파일을 등록한다.
...
<!-- Integration Apache Commons Validator by Spring Modules -->
<bean id="beanValidator" class="org.springmodules.validation.commons.DefaultBeanValidator">
<property name="validatorFactory" ref="validatorFactory"/>
</bean>
<bean id="validatorFactory" class="org.springmodules.validation.commons.DefaultValidatorFactory">
<property name="validationConfigLocations">
<list>
<!-- validator-rules.xml, validator.xml의 위치-->
<value>/WEB-INF/conf/validator-rules.xml</value>
<value>/WEB-INF/conf/validator.xml</value>
</list>
</property>
</bean>
...
validator-rules.xml은 application에서 사용하는 모든 validation rule에 대해 정의하는 파일이다. 예제에 있는 validator-rules.xml의 필수값 체크 부분을 보면 아래와 같이 작성되어 있다.
<validator name="required"
classname="org.springmodules.validation.commons.FieldChecks"
method="validateRequired"
methodParams="java.lang.Object,
org.apache.commons.validator.ValidatorAction,
org.apache.commons.validator.Field,
org.springframework.validation.Errors"
msg="errors.required">
<javascript><![CDATA[
.....
]]>
</javascript>
</validator>
| name | validation rule(required,mask,integer,email…) |
|---|---|
| classname | validation check를 수행하는 클래스명(org.springmodules.validation.commons.FieldChecks) |
| method | validation check를 수행하는 클래스의 메소드명(validateRequired,validateMask…) |
| methodParams | validation check를 수행하는 클래스의 메소드의 파라미터 |
| msg | 에러 메시지 key |
| javascript | client-side validation을 위한 자바스크립트 코드 |
Spring Modules에서 제공하는 validation rule에 따라 구성해보자.
Spring Modules (0.9 기준)에서 제공하는 validation rule들은 아래와 같다.
| name(validation rule) | FieldCheck 클래스 | FieldCheck 클래스의 메소드 | 기능 |
|---|---|---|---|
| required | org.springmodules.validation.commons.FieldChecks | validateRequired | 필수값 체크 |
| minlength | org.springmodules.validation.commons.FieldChecks | validateMinLength | 최소 길이 체크 |
| maxlength | org.springmodules.validation.commons.FieldChecks | validateMaxLength | 최대 길이 체크 |
| mask | org.springmodules.validation.commons.FieldChecks | validateMask | 정규식 체크 |
| byte | org.springmodules.validation.commons.FieldChecks | validateByte | Byte형 체크 |
| short | org.springmodules.validation.commons.FieldChecks | validateShort | Short형 체크 |
| integer | org.springmodules.validation.commons.FieldChecks | validateInteger | Integer형 체크 |
| long | org.springmodules.validation.commons.FieldChecks | validateLong | Long형 체크 |
| float | org.springmodules.validation.commons.FieldChecks | validateFloat | Float형 체크 |
| double | org.springmodules.validation.commons.FieldChecks | validateDouble | Double형 체크 |
| date | org.springmodules.validation.commons.FieldChecks | validateDate | Date형 체크 |
| range | org.springmodules.validation.commons.FieldChecks | validateIntRange | 범위 체크 |
| intRange | org.springmodules.validation.commons.FieldChecks | validateIntRange | int형 범위 체크 |
| floatRange | org.springmodules.validation.commons.FieldChecks | validateFloatRange | Float형 범위체크 |
| creditCard | org.springmodules.validation.commons.FieldChecks | validateCreditCard | 신용카드번호체크 |
| org.springmodules.validation.commons.FieldChecks | validateEmail | 이메일체크 |
validator-rules.xml을 직접 작성하는것 보다는 예제에 있는 파일을 참고하거나 copy해서 사용하면 편리하다.
spring-modules-0.9.zip을 압축을 풀어보면 예제코드가 있다.(\samples\sources\spring-modules-validation-commons-samples-src.zip)
예제코드의 validator-rules.xml(\webapp\WEB-INF\validator-rules.xml)에는 모든 validation rule이 이미 정의되어 있다.
org.springmodules.validation.commons.FieldChecks 클래스에는 필수값 체크, primitive 타입 체크등 여러 validation을 수행하는 메소드들이 있다.
FieldChecks 클래스 소스를 열어 보면 실제 validation 처리는 Commons Validator에 위임하고 있다.
따라서 주의 할점은 Commons Validator에서 제공하는 validation rule중에, FieldChecks 클래스가 제공하지 않는 rule도 있다는 것이다.
예를 들어 Commons Validator 1.3.1 에서는 URL이나 IP 관련 Validator를 제공하지만, Spring Modules의 FieldChecks 클래스에 해당 메소드가 없기 때문에,
새로운 FieldCheck 클래스를 추가한 후 validation-rules.xml에 클래스와 메소드를 등록해야 사용할 수 있다.
Spring Modules가 제공하는 validation rule외에 rule을 추가하는 방법에 대해서는 주민등록번호 validation rule 추가하기 를 참고하라.
validator.xml은 validation rule과 validation할 Form을 매핑한다.
form name과 field property의 name-rule은 Server-side와 Client-side인 경우에 따라 다르다.
Server-side validation의 경우는,
form name과 field property는 validation할 폼 클래스의 이름, 필드과 각각 매핑된다.(camel case)
폼 클래스가 Employee면 employee, DepartmentForm 이면 departmentForm을 form name으로 지정하라.
Client-side의 경우는,
form name은 JSP에서 설정한 <validator:javascript formName=“employee” …/> 태그의 formName와 매핑되고, field property는 각각의 폼 필드의 이름과 일치하면 된다.
따라서, Server-side, Client-side 둘 다 수행하려면,
JSP의 <validator:javascript formName=“employee” …/> 태그의 formName은 폼 클래스의 이름이 되어야 하고, JSP의 폼 필드들은 폼 클래스의 필드와 일치해야 한다.
depends는 해당 필드에 적용할 (validator-rules.xml에 정의된 rule name) validator를 의미한다.
<arg key…>는 메시지 출력시 파라미터를 지정하는 값이다.
아래와 같이 작성했다면, Employee 클래스의 name 필드에 대해서 필수값 체크를, age 필드에 대해서 integer 체크를, email 필드에 대해선 필수값과 email 유효값 체크를 하겠다는 의미이다
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE form-validation PUBLIC
"-//Apache Software Foundation//DTD Commons Validator Rules Configuration 1.1//EN"
"http://jakarta.apache.org/commons/dtds/validator_1_1.dtd">
<form-validation>
<formset>
<form name="employee">
<field property="name" depends="required">
<arg0 key="employee.name" />
</field>
<field property="age" depends="integer">
<arg0 key="employee.age" />
</field>
<field property="email" depends="required,email">
<arg0 key="employee.email" />
</field>
</form>
</formset>
</form-validation>
Server-Side Validation을 위해 Controller에 validation 로직을 추가해 보자.
위 설정 파일에서 등록한 DefaultBeanValidator가 Controller에서 validation을 수행한다.
DefaultBeanValidator는 Commons Validator의 기능을 사용하지만 자신은 Spring Validator이기 때문에,
사용자는 Spring Validator를 사용하듯이 Controller에서 validation 코딩을 하면 된다.
package com.easycompany.controller.annotation;
...
import org.springmodules.validation.commons.DefaultBeanValidator;
...
@Controller
public class UpdateEmployeeController {
...
@Autowired
private DefaultBeanValidator beanValidator;
...
@RequestMapping(value = "/updateEmployee.do", method = RequestMethod.POST)
public String updateEmployee(@ModelAttribute("employee") Employee employee,
BindingResult bindingResult, Model model) {
beanValidator.validate(employee, bindingResult); //validation 수행
if (bindingResult.hasErrors()) { //만일 validation 에러가 있으면...
.....
return "modifyemployee";
}
employeeManageService.updateEmployee(employee);
return "changenotify";
}
}
Validation을 적용할 JSP를 작성한다.(modifyemployee.jsp)
form submit을 하면 이름, 나이, 이메일등의 입력값이 Employee 클래스에 바인딩이 되서 Controller에 전달이 되고,
Controller에 validation 수행 로직이 있으면 validator.xml 내용에 따라 validation이 이루어 진다.
만일 에러가 발생하면 <form:error…/>에 에러에 해당하는 메시지를 출력한다. 에러 메시지에 관련해서는 아래 에러 메시지 등록을 참고하라.
<form:form commandName="employee">
<table>
....
<tr>
<th>이름</th>
<td><form:input path="name" size="20"/><form:errors path="name" /></td>
</tr>
<tr>
<th>비밀번호</th>
<td><form:input path="password" size="10"/></td>
</tr>
<tr>
<th>나이</th>
<td><form:input path="age" size="10"/><form:errors path="age" /></td>
</tr>
<tr>
<th>이메일</th>
<td><form:input path="email" size="50"/><form:errors path="email" /></td>
</tr>
</table>
<table width="80%" border="1">
<tr>
<td>
<input type="submit"/>
<input type="button" value="LIST" onclick="location.href='/easycompany/employeeList.do'"/>
</td>
</tr>
</table>
</form:form>
메시지 파일에 에러 메시지를 등록한다.
validation 에러가 발생하면 validator-rules.xml에서 정의했던 msg값으로 에러메시지의 key값을 찾아 해당하는 메시지를 출력해준다.
예를 들어, email validation에서 에러가 발생하면 msg값이 errors.email 이므로, “유효하지 않은 이메일 주소입니다.”라는 에러 메시지를 JSP에 있는 <form:errors path=“email” /> 부분에 출력하게 된다.
employee.name=이름
employee.email=이메일
employee.age=나이
employee.password=비밀번호
# -- validator errors --
errors.required={0}은 필수 입력값입니다.
errors.minlength={0}은 {1}자 이상 입력해야 합니다.
errors.maxlength={0}은 {1}자 이상 입력할수 없습니다.
errors.invalid={0}은 유효하지 않은 값입니다.
errors.byte={0}은 byte타입이어야 합니다.
errors.short={0}은 short타입이어야 합니다.
errors.integer={0}은 integer 타입이어야 합니다.
....
errors.email=유효하지 않은 이메일 주소입니다.
이름 필드에 값을 비우고 submit하면, name에 필수값(required) validation rule이 설정되어 있으므로 아래와 같이 이름 필드 옆에 에러 메시지가 출력 될 것이다.

아래와 같은 내용의 validator.jsp를 작성한다.
<%@ page language="java" contentType="javascript/x-javascript" %>
<%@ taglib prefix="validator" uri="http://www.springmodules.org/tags/commons-validator" %>
<validator:javascript dynamicJavascript="false" staticJavascript="true"/>
/validator.do로 호출하도록 Controller에서 메소드를 추가하고 requestmapping 한다.
validator.jsp를 http://localhost:8080/easycompany/validator.do 브라우져에서 호출해보면, validator-rules.xml에서 정의한 javascript 함수들이 다운로드 되거나 화면에 print 되는 걸 확인할 수 있을 것이다.
validator.jsp는 client-validation을 위해 validator-rules.xml에서 정의한 javascript 함수들을 로딩한다.
따라서 client-validation을 할 페이지에서는 이 validator.jsp을
<script type="text/javascript" src="<c:url value="/validator.do"/>"></script>
같이 호출한다.
client-validation을 위해서는 해당 JSP에 아래와 같은 작업이 추가 되어야 한다. commons-validator taglib를 선언한다.
<%@ taglib prefix="validator" uri="http://www.springmodules.org/tags/commons-validator" %>
필요한 자바 스크립트 함수를 generate 하기 위한 코드를 추가 한다. validation-rules.xml에서 선언한 함수를 불러 오기 위해, 위에서 작성한 validator.jsp를 아래와 같이 호출한다.
<script type="text/javascript" src="<c:url value="/validator.do"/>"></script>
위의 자바 스크립트 함수를 이용해 필요한 validation과 메시지 처리를 위한 자바 스크립트를 generate 하기 위한 코드를 추가 한다. formName에는 validator.xml에서 정의한 form의 이름을 써준다.
<validator:javascript formName="employee" staticJavascript="false" xhtml="true" cdata="false"/>
form submit시에 validateVO클래스명() 함수를 호출한다.
... onsubmit="return validateEmployee(this)" ">
따라서 앞의 server-side validation에서 작성한 modifyemployee.jsp은 아래와 같이 변경된다.
<!-- commons-validator tag lib 선언-->
<%@ taglib prefix="validator" uri="http://www.springmodules.org/tags/commons-validator" %>
....
<!--for including generated Javascript Code(in validation-rules.xml)-->
<script type="text/javascript" src="<c:url value="/validator.do"/>"></script>
<!--for including generated Javascript Code(validateEmployee(), formName:validator.xml에서 정의한 form의 이름)-->
<validator:javascript formName="employee" staticJavascript="false" xhtml="true" cdata="false"/>
<script type="text/javascript">
function save(form){
if(!validateEmployee(form)){
return;
}else{
form.submit();
}
}
</script>
....
<form:form commandName="employee">
<table>
....
<tr>
<th>이름</th>
<td><form:input path="name" size="20"/><form:errors path="name" /></td>
</tr>
<tr>
<th>비밀번호</th>
<td><form:input path="password" size="10"/></td>
</tr>
<tr>
<th>나이</th>
<td><form:input path="age" size="10"/><form:errors path="age" /></td>
</tr>
<tr>
<th>이메일</th>
<td><form:input path="email" size="50"/><form:errors path="email" /></td>
</tr>
</table>
<table width="80%" border="1">
<tr>
<td>
<!--<input type="submit"/>-->
<input type="button" value="SAVE" onclick="save(this.form)"/> <!-- client-validation을 위해 바로 submit하지 않고 먼저 validateEmployee 함수를 호출-->
<input type="button" value="LIST" onclick="location.href='/easycompany/employeeList.do'"/>
</td>
</tr>
</table>
</form:form>
....
이번에도 이름 필드의 값을 지우고 저장 버튼을 누르면 아래와 같은 alert 메시지가 보일 것이다.

Commons Validator는 primitive type, 필수값, 이메일등 흔히 사용되는 유형에 대한 validation rule을 template으로 제공하지만,
프로젝트의 특성에 따라 공통으로 사용되는 validation rule이 발생되고 이를 추가해야할 필요가 생길 수 있다.
공공프로젝트에서 흔히 사용되는 주민등록번호 validator를 추가해 봄으로써, validation rule을 추가하는 방법을 알아보고자 한다.
예제는 easycompany를 이용했다.
Spring Module을 이용해서 Commons Validator를 사용한다면 아래와 같은 내용을 validation rule 정의 파일(validator-rules.xml 같은)에서 보았을 것이다.
<!--필수값 체크 validation rule-->
<validator name="required"
classname="org.springmodules.validation.commons.FieldChecks"
method="validateRequired"
methodParams="java.lang.Object,
org.apache.commons.validator.ValidatorAction,
org.apache.commons.validator.Field,
org.springframework.validation.Errors"
msg="errors.required">
<javascript><![CDATA[
...
]]>
</javascript>
</validator>
validator 태그의 각각의 attribute는 다음과 같은 의미를 같는다.
| name | validation rule(required,mask,integer,email…) |
|---|---|
| classname | validation check를 수행하는 클래스명 |
| method | validation check를 수행하는 클래스의 메소드명 |
| methodParams | validation check를 수행하는 클래스의 메소드의 파라미터 |
| msg | 에러 메시지 key |
| javascript | client-side validation을 위한 자바스크립트 코드 |
주민등록번호 rule을 아래와 같이 추가한다고 하면,
<validator name="ihidnum"
classname="egovframework.rte.ptl.mvc.validation.RteFieldChecks"
method="validateIhIdNum"
methodParams="java.lang.Object,
org.apache.commons.validator.ValidatorAction,
org.apache.commons.validator.Field,
org.springframework.validation.Errors"
depends=""
msg="errors.ihidnum">
<javascript><![CDATA[
...
]]>
</javascript>
</validator>
필요한 작업은 아래와 같다.
org.springmodules.validation.commons.FieldChecks를 상속 받는 RteFieldChecks 클래스를 생성한다. 그리고 주민등록번호 validation을 담당할 validateIhIdNum 메소드를 추가한다. 주민등록번호 validation 로직이 RteFieldChecks.validateIhIdNum() 안에 있어도 되지만, org.springmodules.validation.commons.FieldChecks와 같은 방식으로 다른 Validator에 위임했다.
package egovframework.rte.ptl.mvc.validation;
import org.apache.commons.validator.Field;
import org.apache.commons.validator.ValidatorAction;
import org.springframework.validation.Errors;
import org.springmodules.validation.commons.FieldChecks;
public class RteFieldChecks extends FieldChecks{
public static boolean validateIhIdNum(Object bean, ValidatorAction va,
Field field, Errors errors){
//bean에서 해당 field 값을 추출
String ihidnum = FieldChecks.extractValue(bean, field);
//주민등록번호 유효성 검사 알고리즘은 RteGenericValidator가 가지고 있다.
if(!RteGenericValidator.isValidIdIhNum(ihidnum)){ //유효한 주민등록번호가 아니면
FieldChecks.rejectValue(errors, field, va); //에러 처리
return false;
}else{
return true;
}
}
}
주민등록번호 유효성체크 로직이 있는 RteGenericValidator 클래스를 작성해 보자. 유효성 체크 기준은
로 했다.
package egovframework.rte.ptl.mvc.validation;
import java.io.Serializable;
import org.apache.commons.validator.GenericTypeValidator;
public class RteGenericValidator implements Serializable {
public static boolean isValidIdIhNum(String value) {
//값의 길이가 13자리이며, 7번째 자리가 1,2,3,4 중에 하나인지 check.
String regex = "\\d{6}[1234]\\d{6}";
if (!value.matches(regex)) {
return false;
}
//앞 6자리의 값이 유효한 날짜인지 check.
try {
String strDate = value.substring(0, 6);
strDate = ((value.charAt(6) == '1' || value.charAt(6) == '2') ? "19":"20") + strDate;
strDate = strDate.substring(0, 4) + "/" + strDate.substring(4, 6)
+ "/" + strDate.substring(6, 8);
SimpleDateFormat dateformat = new SimpleDateFormat("yyyy/MM/dd");
Date date = dateformat.parse(strDate);
String resultStr = dateformat.format(date);
if (!resultStr.equalsIgnoreCase(strDate)) {
return false;
}
} catch (ParseException e) {
// TODO Auto-generated catch block
e.printStackTrace();
return false;
}
//주민등록번호 마지막 자리를 이용한 check.
char[] charArray = value.toCharArray();
long sum = 0;
int[] arrDivide = new int[] { 2, 3, 4, 5, 6, 7, 8, 9, 2, 3, 4, 5 };
for (int i = 0; i < charArray.length - 1; i++) {
sum += Integer.parseInt(String.valueOf(charArray[i]))
* arrDivide[i];
}
int checkdigit = (int) ((int) (11 - sum % 11)) % 10;
return (checkdigit == Integer.parseInt(String.valueOf(charArray[12]))) ? true
: false;
}
}
이제 주민등록번호 validation rule을 추가해 보자. rule 이름은 ihidnum으로 하고 위에서 작성한 코드를 바탕으로 설정하면, (이미 위 개요에 나온 대로) 아래와 같다. client-validation을 위해서 자바스크립트 코드도 추가했다.
<validator name="ihidnum"
classname="egovframework.rte.ptl.mvc.validation.RteFieldChecks"
method="validateIhIdNum"
methodParams="java.lang.Object,
org.apache.commons.validator.ValidatorAction,
org.apache.commons.validator.Field,
org.springframework.validation.Errors"
depends=""
msg="errors.ihidnum">
<javascript><![CDATA[
function validateIhIdNum(form) {
var bValid = true;
var focusField = null;
var i = 0;
var fields = new Array();
oIhidnum = new ihidnum();
for (x in oIhidnum) {
var field = form[oIhidnum[x][0]];
if (field.type == 'text' ||
field.type == 'hidden' ||
field.type == 'textarea') {
if (trim(field.value).length==0 || !checkIhIdNum(field.value)) {
if (i == 0) {
focusField = field;
}
fields[i++] = oIhidnum[x][1];
bValid = false;
}
}
}
if (fields.length > 0) {
alert(fields.join('\n'));
}
return bValid;
}
/**
* Reference: JS Guide
* http://jsguide.net/ver2/articles/frame.php?artnum=002
*/
function checkIhIdNum(ihidnum){
fmt = /^\d{6}[1234]\d{6}$/;
if(!fmt.test(ihidnum)){
return false;
}
birthYear = (ihidnum.charAt(7) <= "2") ? "19" : "20";
birthYear += ihidnum.substr(0, 2);
birthMonth = ihidnum.substr(2, 2) - 1;
birthDate = ihidnum.substr(4, 2);
birth = new Date(birthYear, birthMonth, birthDate);
if( birth.getYear() % 100 != ihidnum.substr(0, 2) ||
birth.getMonth() != birthMonth ||
birth.getDate() != birthDate) {
return false;
}
var arrDivide = [2, 3, 4, 5, 6, 7, 8, 9, 2, 3, 4, 5];
var checkdigit = 0;
for(var i=0;i<ihidnum.length-1;i++){
checkdigit += parseInt(ihidnum.charAt(i)) * parseInt(arrDivide[i]);
}
checkdigit = (11 - (checkdigit%11))%10;
if(checkdigit != ihidnum.charAt(12)){
return false;
}else{
return true;
}
}
]]>
</javascript>
</validator>
ihidnum이란 필드에 주민등록번호 validation 체크를 해보자. validator.xml에 필요한 내용을 추가하고.
<form-validation>
<formset>
<form name="employee">
...
<field property="ihidnum" depends="ihidnum"/>
...
</form>
</formset>
</form-validation>
messages 프로퍼티에 에러메시지를 추가하자.
errors.ihidnum=유효하지 않은 주민등록번호입니다.
주민등록번호 server-side,client-side validatin을 위한 환경은 다 갖추어졌다. 이제 EmployeeController에 validation을 추가하고,(이미 추가되어 있다면 pass)
package com.easycompany.controller.annotation;
...
import org.springmodules.validation.commons.DefaultBeanValidator;
...
@Controller
public class EmployeeController {
...
@Autowired
private DefaultBeanValidator beanValidator;
...
@RequestMapping(value = "/updateEmployee.do", method = RequestMethod.POST)
public String updateEmployee(@ModelAttribute("employee") Employee employee,
BindingResult bindingResult, Model model) {
beanValidator.validate(employee, bindingResult); //validation 수행
if (bindingResult.hasErrors()) { //만일 validation 에러가 있으면...
.....
return "modifyemployee";
}
employeeManageService.updateEmployee(employee);
return "changenotify";
}
}
VO Class(com.easycompany.model.Employee.java)에 주민등록번호 필드를 추가하고,
package com.easycompany.model;
public class Employee {
....
private String ihidnum;
private String ihidnum1;
private String ihidnum2;
...
public String getIhidnum() {
return ihidnum1+ihidnum2;
}
public String getIhidnum1() {
return ihidnum1;
}
public void setIhidnum1(String ihidnum1) {
this.ihidnum1 = ihidnum1;
}
public String getIhidnum2() {
return ihidnum2;
}
public void setIhidnum2(String ihidnum2) {
this.ihidnum2 = ihidnum2;
}
}
JSP(/easycompany/webapp/jsp/modifyemployee.jsp)에 주민등록번호 입력 필드를 추가하자.
...
<tr>
<th>주민번호</th>
<td>
<form:input path="ihidnum1" size="10"/> - <form:input path="ihidnum2" size="10"/><form:errors path="ihidnum" />
<form:hidden path="ihidnum"/>
</td>
</tr>
...
주민등록번호를 입력하지 않거나, 틀린번호를 입력시엔 아래와 같은 경고창이 뜬다.

틀린 입력값으로 client를 통과하더라도 Controller에서 validation이 추가로 동작하므로, server-side에서 validation error가 일어날것이다.
전자정부 표준프레임워크와 UI 솔루션(Rich Internet Application) 연동에 대해 살펴 본다. UI Adaptor를 적용하는 방식은 특정한 하나의 방법을 표준화하기 어렵다. 보통 Web Framework 과 UI 솔루션과의 연동을 하는 방법 중 가장 많이 사용하는 방식은 Controller 역할을 수행하는 Servlet 객체에서 업무 로직을 호출 전 데이터를 DTO 형태로 변화하여 업무 로직으로 넘기는 방식이다.
전자정부 표준프레임워크에서는 Spring MVC Annotation 기반으로 개발 시 요청되는 URI 와 Controller 클래스내의 메소드를 매핑하고 있다. 따라서 메소드의 파라미터로 넘어오는 객체가 request 객체가 아닌 업무용 DTO 클래스로 넘어올 수 있도록 가이드 하는 방식을 선택했다. (사실 @ModelAttribute 를 이용하는 것과 같다.) 하지만 프로젝트 별로 비기능 요구사항의 특성을 고려하여 적합한 구조를 정의하여 적용하는 것이 필요하다.
UI 솔루션 업체별 상세 가이드
중점적으로 우리가 살펴볼 내용은 Controller 앞단에서 UI 솔루션으로부터 넘어온 데이터를 DTO 로 변환하는 과정이다. 데이타 변환을 위해 우리는 ArgumentResolver 를 이용한 방법을 살펴보도록 하겠다. UI 솔루션으로 넘어오는 데이터 객체는 request 객체에 포함되어 넘어온다. 우리가 필요한 것은 업무용 DTO 클래스이다. 업무용 DTO 클래스는 URI(@RequestMapping)와 매핑 된 Controller 메소드의 파라미터로 존재하게 된다. Controller 메소드의 파라미터에 설정된 클래스(여기서는 DTO)를 AnnotationMethodHandlerAdapter 에서 그에 해당하는 ArgumentResolvers(customArgumentResolvers포함) 를 호출해준다. 따라서 우리는 ArgumentResolver를 확장하여 CustomRiaArgumentResolver 개발하여 AnnotationMethodHandlerAdapter에 등록한다. CustomRiaArgumentResolver 에서 리턴되는 객체는 Contorller 단의 메소드의 파라미터로 이용된다.
그리고 Controller단의 실행 결과는 ViewResovler를 통해 RiaView 로 전송되며 RiaVeiw는 결과물인 DTO 를 UI 솔루션 데이터 타입으로 변환하여 response로 보내어 진다. 다시 핵심적인 내용을 정리하자면 다음과 같다.
사용되는 DTO 를 아래 클래스로 생각하고 살펴보겠다.
UdDTO.java(샘플)
...
public class UdDTO implements Serializable {
private Map variableList ;
private Map dataSetList ;
private Map Objects ;
public void setVariableList(Map variableList) {
this.variableList= variableList;
}
public void setDataSetList(Map dataSetList) {
this.dataSetList= dataSetList;
}
public Map getVariableList() {
return variableList;
}
public Map getDataSetList() {
return dataSetList;
}
public void setObjects(Map objects) {
Objects = objects;
}
public Map getObjects() {
return Objects;
}
}
...
다음은 UTO 를 가져와 UI 솔루션에 의해 들어오는 객체로부터 UTO 로 변환하는 부분을 설명한다. 변환을 담당하는 UIAdaptorImpl 객체는 AnnotationMethodHandlerAdapter 의 CustomRiaArgumentResolver 에 설정된다.
설정정보(CustomRiaArgumentResolver)
<bean class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter">
<property name="webBindingInitializer">
<bean class="egovframework.rte.fdl.web.common.EgovBindingInitializer" />
</property>
<property name="customArgumentResolvers">
<list>
<bean class="egovframework.rte.fdl.sale.web.CustomRiaArgumentResolver">
<property name="uiAdaptor">
<ref bean="riaAdaptor" />
</property>
</bean>
</list>
</property>
</bean>
WebArgumentResolver의 구현체인 CustomRiaArgumentResolver 는 uiAdaptor 에 세팅된 Adaptor 를 실행해준다.
CustomRiaArgumentResolver.java
public class CustomRiaArgumentResolver implements WebArgumentResolver {
private UiAdaptor uiA;
public void setUiAdaptor(UiAdaptor uiA) {
this.uiA = uiA;
}
public Object resolveArgument(MethodParameter methodParameter, NativeWebRequest webRequest) throws Exception {
Class<?> type = methodParameter.getParameterType();
Object uiObject = null;
if (uiA == null)
return UNRESOLVED;
//Controller의 실행되는 메소드의 파라미터타입 정보가 MethodParameter를 통해 넘어온다.
//설정한 UIAdaptro 구현체에 등록되어 있는 UTO 와 비교한다.
if (type.equals(uiA.getModelName())) {
HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest();
// 여기서 데이타 만들어 넘긴다.
uiObject = (UdDTO) uiA.convert(request);
return uiObject;
}
return UNRESOLVED;
}
...
UiAdaptor 의 구현체는 아래와 같다. 여기서는 Miplatform 의 예를 들어 코드를 작성하였다. UI 솔루션의 객체에서 DTO 로 데이터를 옮기는 역할은 converte4In 메소드에서 수행된다.
RiaAdaptorImpl.java(MiPlatform ⇒ UdDTO)
public class RiaAdaptorImpl implements UiAdaptor {
protected Log log = LogFactory.getLog(this.getClass());
//resolveArgument 메소드에서 호출하는 메소드
public Object convert(HttpServletRequest request) throws Exception {
PlatformRequest platformRequest = null;
try {
platformRequest = new PlatformRequest(request, PlatformRequest.CHARSET_UTF8);
platformRequest.receiveData();
} catch (IOException ex) {
ex.getStackTrace();
// throw new IOException("PlatformRequest error");
}
//UI 솔루션 데이터에서 DTO 객체로 변환
UdDTO dto = converte4In(platformRequest);
return dto;
}
private UdDTO converte4In(PlatformRequest platformRequest) {
UdDTO dto = new UdDTO();
//... DTO 또는 VO 값 채우기
.....
return dto;
}
public Class getModelName() {
return UdDTO.class;
}
}
UdDTO 클래스는 CustomRiaArgumentResolver 에서 만들어져 Controller 의 메소드의 parameter 형태로 가져온다. 아래 예는 Controller 단의 메소드 이다.
XXCategoryController.java
...
@RequestMapping("/sample/miplatform.do")
public ModelAndView selectCategoryList4Mi(UdDTO miDto, Model model) throws Exception {
ModelAndView mav = new ModelAndView("riaView");
//조회조건이 있을 경우 사용될 Map
Map<String, String> smp = new HashMap<String, String>();
try {
//Biz Layer 를 호출 한다.
List resultList = categoryService.selectCategoryList(smp);
//결과값을 모델에 저장
mav.addObject("MiDTO", resultList);
} catch (Exception ex) {
log.info(ex.getStackTrace(), ex);
}
return mav;
}
모델 객체의 이름이 riaView 이다. 이것은 Bean Name을 직접 명시한 것으로 아래와 같은 설정(BeanNameViewResolver)이 필요하다.
<bean class="org.springframework.web.servlet.view.BeanNameViewResolver" p:order="0" />
<bean class="org.springframework.web.servlet.view.UrlBasedViewResolver"
p:order="1" p:viewClass="org.springframework.web.servlet.view.JstlView"
p:prefix="/WEB-INF/jsp/" p:suffix=".jsp" />
<bean id="riaView" class="egovframework.rte.fdl.sale.web.RiaView" />
RiaView 의 코드는 아래와 같다. DTO 를 업체에 맞춰 다시 가져 나가기 위해 convert 하는 모듈이다. 여기서는 Miplatform 객체로 변환한다. egovDs 라는 DataSet 으로 객체화 한후 stream 형태로 보내는 로직이다. 따라서 업체별 데이타 형태로 변환하여 보내도록 수정하면 된다.
RiaView.java(DTO ⇒ MiPlatform)
....
public class RiaView extends AbstractView {
protected Log log = LogFactory.getLog(this.getClass());
@SuppressWarnings("unchecked")
@Override
protected void renderMergedOutputModel(Map model, HttpServletRequest request, HttpServletResponse response)
throws Exception {
VariableList miVariableList = new VariableList();
DatasetList miDatasetList = new DatasetList();
PlatformData platformData = new PlatformData(miVariableList, miDatasetList);
List list = (List) model.get("MiDTO");
Iterator<Map> iterator = list.iterator();
Iterator<Map> dataIterator = list.iterator();
Dataset dataset = new Dataset("egovDs");
while (iterator.hasNext()) {
// Header 세팅
Map<String, Object> record = iterator.next();
Iterator<String> si = record.keySet().iterator();
while (si.hasNext()) {
String key = si.next();
dataset.addColumn(key, ColumnInfo.COLUMN_TYPE_STRING, (short) 255);
}
}
while (dataIterator.hasNext()) {
Map<String, Object> record = dataIterator.next();
Iterator<String> si = record.keySet().iterator();
// Header 세팅
while (si.hasNext()) {
String key = si.next();
dataset.addColumn(key, ColumnInfo.COLUMN_TYPE_STRING, (short) 255);
}
// Value 세팅
int row = dataset.appendRow();
Iterator<String> si2 = record.keySet().iterator();
while (si2.hasNext()) {
String key = si2.next();
String value = (String) record.get(key);
System.out.println("key = " + key + " , value = " + value);
dataset.setColumn(row, key, value);
}
miDatasetList.add(dataset);
}
try {
new PlatformResponse(response, PlatformConstants.CHARSET_UTF8).sendData(platformData);
} catch (IOException ex) {
if (log.isErrorEnabled()) {
log.error("Exception occurred while writing xml to MiPlatform Stream.", ex);
}
throw new Exception();
}
}
}
Callable, DeferredResult, WebAsyncTask 등을 사용해 시간이 오래 걸리는 작업을 비동기로 처리하고, 완료 후 응답을 보낼 수 있다.기존의 요청 처리는 하나의 요청에 대해 한 개의 쓰레드를 사용하였다. 하나의 쓰레드에서 요청-응답 과정을 모두 처리하기 때문에 요청처리 이후 응답이 오기까지 쓰레드를 대기상태로 유지하였다. 그러나 서버와의 연결을 유지한채 대기상태로 있는 것이 아니라 서버와의 처리를 계속 이어가게 해주기 위해서는 이러한 기존의 처리에 한계가 있었다.
Servlet 3.0에서 제공하는 비동기 요청 처리는 쓰레드가 대기상태로 있는 것이 아니라 요청을 처리하는 Servlet 쓰레드가 요청후 바로 반환되고 내부의 다른 쓰레드가 이를 처리했다가 처리완료 후 응답처리 리소스가 가용할 때 Servlet쓰레드가 응답처리를 계속 이어가게 해 주는 것이다.
비동기 요청처리를 위해 다음과 같은 환경이 필요하다.
<!-- Servlet -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.0.1</version>
<scope>provided</scope>
</dependency>
web.xml의 servlet버전을 변경해야한다.
<web-app xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
version="3.0">
...
</web-app>
web.xml내의 servlet설정에 async-supported 태그 값을 true로 설정한다.
<servlet>
<servlet-name>appServlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/spring/appServlet/servlet-context.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
<async-supported>true</async-supported>
</servlet>
Spring의 비동기 요청처리에서는 Servlet 쓰레드는 요청을 처리하고 Spring의 Async 제공 클래스를 통해 비동기모드로 전환되며 Servlet 쓰레드는 반환된다. 그 후 내부 Application의 쓰레드에서 서비스 처리가 일어나게 된다. 처리완료 후 Servlet 쓰레드가 다시 응답을 받아 이를 클라이언트로 전송한다.
Spring의 비동기 요청처리에서 제공하는 Class는 Servlet 쓰레드가 반환된 이후 내부 서비스를 처리하는 쓰레드의 종류에 따라 나뉜다.
요청을 처리하는 Servlet 쓰레드가 반환되면 Spring MVC에서 제어하는 쓰레드에 의해 비동기처리된다.
Callable의 처리과정은 다음과 같다.
Callable은 주로 요청처리가 오래걸리는 DB작업, REST API 요청처리를 하는 데 적합하다.
@RequestMapping(“/view”)
public Callable<String> callableWithView(final Model model) {
return new Callable<String>() {
@Override
public String call() throws Exception {
Thread.sleep(2000);
model.addAttribute(“foo”, “bar”);
model.addAttribute(“fruit”, “apple”);
return “view”;
}
}
Servlet 스레드는 반환하고 Spring MVC가 제어하지 않는 쓰레드를 통해 비동기를 처리한다. DefferedResult는 JMS, AMQP, 스케쥴러, Redis, 다른 HTTP요청에서 사용된다.
DefferedResult의 처리과정은 다음과 같다.
@RequestMapping("/quotes")
@ResponseBody
public DeferredResult<String> quotes() {
DeferredResult<String> deferredResult = new DeferredResult<String>();
// Save the deferredResult in in-memory queue ...
queue.add(defferedResult);
return deferredResult;
}
// In some other thread...
@RequestMapping("/someEvent")
@ResponseBody
public String someEvent(String data) {
for(DefferedResult<String> result : queue) {
result.setResult(data);
}
return "view";
}
Callable과 동일한 방식으로 사용하며 Controller에서 Callable을 담아서 반환한다.
Timeout을 추가할 수 있으며 AsyncTaskExecutor를 지정하거나 작업의 종류에 따라 쓰레드 풀을 분리하여 사용할 수 있다.
@RequestMapping("/facebooklink")
public WebAsyncTask<String> facebooklink() {
return new WebAsyncTask<String>(
30000L, // Timeout
"facebookTaskExecutor", // TaskExecutor
new Callable<String>() {
@Override
public String call() throws Exception {
// 작업
return result;
}
}
);
}
$.ajax(), $.get(), $.post()와 같은 메서드를 사용하여 서버와 데이터를 비동기적으로 통신하며, JSON과 같은 형식으로 데이터를 주고받을 수 있다.jQuery는 브라우저 호환성이 있는 다양한 기능을 제공하는 자바스크립트 라이브러리이다. jQuery에서 제공하는 오픈 라이브러리들을 통해 java script로 ajax, event, 다양한 ui 기능 등을 구현할 수 있으며 위키가이드에서는 jQuery의 기본적인 몇가지 기능(ajax, callback함수, post호출 등)에 대하여 살펴본다.
자세한 내용은 jQuery 사이트를 살펴보도록 한다.
jQuery ajax의 다양한 기능들 중 기본Ajax기능과 응용을 통한 콤보박스, Select박스의 간단한 화면처리에 대하여 가이드한다.
jQuery ajax 기본기능
jQuery ajax 응용
jQuery를 이용하기 위해서는 jQuery java script를 추가해주어야 한다. 추가하는 방법은 jquery url을 직접 명시하는 경우, 프로젝트에 jquery java script를 직접 추가하여 참조하는 경우가 있다.
<script src="//code.jquery.com/jquery-1.11.0.min.js"></script>
<script type="text/javascript" src="jQuery파일 경로"></script>
jquery-버전.js를 다운받아 프로젝트 하위경로에 추가한 후, 저장한 경로를 적어준다.
jQuery Ajax기능을 위해서는 기본적으로 jQuery.ajax(url[,settings]) 함수를 이용한다.
jQuery ajax함수 안에는 다음과 같은 설정들이 가능하다.
| 설정 | 설명 | default | type |
|---|---|---|---|
| url | request를 전달할 url명 | N/A | url string |
| data | request에 담아 전달할 data명과 data값 | N/A | String/Plain Object/Array |
| contentType | server로 데이터를 전달할 때 contentType | ‘application/x-www-form-urlencoded; charset=UTF-8’ | contentType String |
| dataType | 서버로부터 전달받을 데이터 타입 | xml, json, script, or html | xml/html/script/json/jsonp/multiple, space-separated values |
| statusCode | HTTP 상태코드에 따라 분기처리되는 함수 | N/A | 상태코드로 분리되는 함수 |
| beforeSend | request가 서버로 전달되기 전에 호출되는 콜백함수 | N/A | Function( jqXHR jqXHR, PlainObject settings ) |
| error | 요청을 실패할 경우 호출되는 함수 | N/A | Function( jqXHR jqXHR, String textStatus, String errorThrown ) |
| success | 요청에 성공할 경우 호출되는 함수 | N/A | Function( PlainObject data, String textStatus, jqXHR jqXHR ) |
| crossDomain | crossDomain request(jsonP와 같은)를 강제할 때 설정(cross-domain request설정 필요) | same-domain request에서 false, cross-domain request에서는 true | Boolean |
하나의 파라미터를 ajax request로 전달하는 예제는 다음과 같다. example01.do로 호출을 하며 sampleInput이란 데이터명으로 “sampleData” String을 전달한다. 요청이 성공할 경우 success의 함수를 호출하며 실패시 error함수를 호출하는 자바스크립트 코드이다.
$.ajax({
url : "<c:url value='/example01.do'/>",
data : {
sampleInput : "sampleData"
},
success : function(data, textStatus, jqXHR) {
//Sucess시, 처리
},
error : function(jqXHR, textStatus, errorThrown){
//Error시, 처리
}
});
ajax의 콜백함수이다. jQuery 1.5부터 jQuery의 모든 Ajax함수는 XMLHttpRequest객체의 상위 집합을 리턴받을 수 있게 되었다. 이 객체를 jQuery에서는 jqXHR이라 부르며, jqXHR의 함수로 콜백함수를 정의할 수 있다.
아래 콜백함수와 위의 settings에서 정의된 error, success콜백함수와 다른 점은 다음과 같다.
| 함수명 | 설명 |
|---|---|
jqXHR.done(function( data, textStatus, jqXHR ) {}); | 성공시 호출되는 콜백함수 |
jqXHR.fail(function( jqXHR, textStatus, errorThrown ) {}); | 실패시 호출되는 콜백함수 |
jqXHR.always(function( data|jqXHR, textStatus, jqXHR'|errorThrown ) { }); | 항상 호출되는 콜백함수 |
여러개의 데이터를 전달하며 호출 후 콜백함수로 서버에서 값을 받는 예제이다. example02.do을 호출하며 name, location을 요청데이터로 전달한다. 성공시에 done콜백함수를 호출한다.
$.ajax({
url : "<c:url value='/example02.do'/>",
data : {
name : "gil-dong",
location : "seoul"
},
})
.done(function( data ) {
if ( console && console.log ) {
console.log( "Sample of data:", data.slice( 0, 100 ) );
}
});
example03.do를 호출하며 성공시 done콜백함수를, 실패시 fail콜백함수가 호출된다. 성공,실패여부에 상관없이 always콜백함수는 항상 호출된다. done, fail, always콜백함수는 ajax함수를 통해 리턴되 request로 호출가능하다.
var jqxhr = $.ajax( "<c:url value='/example03.do'/>", )
.done(function() {
alert( "success" );
})
.fail(function() {
alert( "error" );
})
.always(function() {
alert( "complete" );
});
jqxhr.always(function() {
alert( "second complete" );
});
jQuery 1.5부터 sucess콜백함수는 jqXHR(XMLHttpRequest의 상위집합 객체)를 받을 수 있게 되었다. 그러나 JSONP와 같은 cross-domain request의 GET요청 시에는 jqXHR을 사용하여도 XHR인자는 success함수 안에서 undefined로 인식된다.
jQuery.get()은 ajax를 GET요청하는 함수이며 jqXHR을 반환받는다. 따라서 $.ajax()와 동일하게 done, fail, always콜백함수를 쓸 수 있다. get함수는 ajax함수로 나타내면 다음과 같다.
$.ajax({
url: url,
data: data,
success: success,
dataType: dataType
});
| 설정 | 설명 | default | type |
|---|---|---|---|
| url | request를 전달할 url명 | N/A | url String |
| data | request에 담아 전달할 data명과 data값 | N/A | String/Plain Object |
| dataType | 서버로부터 전달받을 데이터 타입 | xml, json, script, or html | String |
| success | 요청에 성공할 경우 호출되는 함수 | N/A | Function( PlainObject data, String textStatus, jqXHR jqXHR ) |
url만 호출하고 결과값은 무시하는 경우
$.get( "example.do" );
url로 데이터만 보내고 결과는 무시하는 경우
$.get( "example.do", { name: "gil-dong", location: "seoul" } );
url를 호출하고 결과값을 Alert창으로 띄우는 경우
$.get( "test.php", function( data ) {
alert( "Data Loaded: " + data );
});
url를 호출하고 결과값을 Alert창으로 띄우는 경우
$.get( "example.do", { name: "gil-dong", location: "seoul" } )
.done(function( data ) {
alert( "Data Loaded: " + data );
});
ajax호출을 HTTP GET메서드로 JSON문자열로 인코딩한 데이터를 요청한다. $.ajax()메서드로 표현하면 다음과 같다.
$.ajax({
url: url,
data: data,
success: success,
dataType: 'json'
});
| 설정 | 설명 | default | type |
|---|---|---|---|
| url | request를 전달할 url명 | N/A | url String |
| data | request에 담아 전달할 data명과 data값 | N/A | String/Plain Object |
| dataType | 서버로부터 전달받을 데이터 타입 | xml, json, script, or html | String |
jQuery.post()은 ajax를 POST요청하는 함수이며 jqXHR을 반환받는다. 따라서 ajax(), get()와 동일하게 done, fail, always콜백함수를 쓸 수 있다. jQuery.post 함수 설정은 get함수와 동일하다.(jQuery.post( url [, data ] [, success ] [, dataType ] ))
jQuery.post함수를 ajax함수로 쓰면 다음과 같다.
$.ajax({
type: "POST",
url: url,
data: data,
success: success,
dataType: dataType
});
url만 호출하고 결과값은 무시하는 경우
$.post( "example.do" );
url로 데이터만 보내고 결과는 무시하는 경우
$.post( "example.do", { name: "gil-dong", location: "seoul" } );
url를 호출하고 결과값을 console log를 남기는 경우
$.post( "example.do", function( data ) {
console.log( data.name );
console.log( data.location );
});
url로 데이터를 호출하고 결과값을 Alert창으로 띄우는 경우
$.post( "example.do", { name: "gil-dong", location: "seoul" } )
.done(function( data ) {
alert( "Data Loaded: " + data );
});
jQuery의 ajax 추가 UI기능(자동완성기능, 판넬 탭 등)을 사용하기 위해서는 jQuery UI를 설정해주어야 한다. 다음에서는 jQuery UI와 ajax를 이용한 자동완성기능(autocomplete), 판넬 탭(tabs)에 대하여 가이드한다.
jQuery UI(version 1.11.0)을 추가하기 위해서는 위에서 설정했던 기본 jQuery script와 jQuery ui스크립트를 다음과 같이 jsp에 추가해준다.
...
<link rel="stylesheet"
href="http://code.jquery.com/ui/1.11.0/themes/smoothness/jquery-ui.css" />
<script src="//code.jquery.com/jquery-1.11.0.min.js"></script>
<script src="http://code.jquery.com/ui/1.11.0/jquery-ui.js"></script>
...
jQuery UI script를 직접 추가하여 참조하는 경우는 jQuery-ui.js와 jQuery-ui.css를 다운받아 프로젝트 하위 경로에 추가한 후, 저장한 경로를 지정해준다.
jQuery에서는 input창에서 예상되는 텍스트값을 보여주는 자동완성기능을 쉽게 구현할 수 있도록 autoComplete()을 제공하고 있다.
| 구분 | 설정 | 설명 | Type |
|---|---|---|---|
| Options | source | 하단에 뜨는 자동완성리스트(필수값) | Array, String, function |
| Options | minLength | 자동완성이 동작하는 최소 문자열 수 | Integer |
| Options | disabled | disable 여부 | Boolean |
| Events | change(event, ui) | 값 변경시 발생하는 이벤트 함수 | autocompletechange |
| Events | focus( event, ui ) | 값이 포커스될 때 발생하는 이벤트 함수 | autocompletefocus |
| Events | select( event, ui ) | 값이 선택될 때 발생하는 이벤트 함수 | autocompleteselect |
| 자세한 내용은 jQuery 사이트의 autocomplete api을 참고한다. |
기본 autoComplete기능 구현은 다음과 같다.
<html lang="en">
<head>
<meta charset="utf-8">
<title>jQuery UI Autocomplete - Default functionality</title>
<link rel="stylesheet" href="//code.jquery.com/ui/1.11.0/themes/smoothness/jquery-ui.css">
<script src="//code.jquery.com/jquery-1.10.2.js"></script>
<script src="//code.jquery.com/ui/1.11.0/jquery-ui.js"></script>
<link rel="stylesheet" href="/resources/demos/style.css">
<script>
$(function() {
var availableTags = [ "ActionScript", "AppleScript", "Asp", "BASIC",
"C", "C++", "Clojure", "COBOL", "ColdFusion", "Erlang",
"Fortran", "Groovy", "Haskell", "Java", "JavaScript", "Lisp",
"Perl", "PHP", "Python", "Ruby", "Scala", "Scheme" ];
$( "#tags" ).autocomplete({
source: availableTags
});
});
</script>
</head>
<body>
<div class="ui-widget">
<label for="tags">Tags: </label>
<input id="tags">
</div>
</body>
</html>
위와 같이 했을 때 결과는 다음과 같다.

minLength는 default값이 1이기 때문에 input에 1개이상의 문자를 입력했을 때 source의 String배열들이 자동문자리스트로 뜨게 된다.
ajax를 통해 리스트를 받아와 autoComplete의 source로 뿌려주는 예제에 대해 살펴보자.ajax를 통해 source를 가져오기 위해서는 서버호출 결과값이 2가지 중 하나의 타입이어야 한다.
...
<script type="text/javaScript">
$(function() {
$('#autoValue').autocomplete(
{
source : function(request, response) {
$.ajax({
url : "<c:url value='/example.do'/>",
data : { input : request.term },
success : function(data) {
response( data.locations );
}
});
},
minLength : 1,
select : function(event, ui) {
alert( ui.item ? "Selected : " + ui.item.label
: "Nothing select, input was " + this.value);
}
});
});
</script>
//... 생략
<input type="text" id="autoValue" />
//... 생략
data로 전송하는 request.term은 input값으로 사용자가 입력한 값이다. 즉, 사용자가 “je”를 입력하면 input = je 로 값이 넘어간다.이 때 Controller에서는 reqeust.getParameter(“input”) 또는 @RequestParam(“input”) String input으로 값을 꺼낼 수 있다.
다음은 MappingJacksonJsonView의 빈을 jsonView로 등록했을 때 Controller에서 data를 꺼내고 결과값을 client로 넘겨주는 예제이다.(Ajax통신 시 java코드는 <Ajax support java code 위키>를 참고한다.)
@RequestMapping(value="/autoList.do")
public String autoList(HttpServletRequest request, ModelMap model) {
String input = request.getParameter("input");
List<String> resultList = new ArrayList<String>();
//...생략...
//서비스클래스를 통해 결과값을 resultList에 담음
model.addAttribute("locations", resultList );
return "jsonView";
}
이 때, 호출되는 Query문의 예이다. (mybatis예제)
<select id="selectLocationList" parameterType="string" resultType="string">
SELECT
LOCATION_NM
FROM LOCATION
WHERE upper(LOCATION_NM) LIKE '%' || upper(#{input}) || '%'
</select>
만약 결과값이 다음과 같다면
{"locations":["Daejeon","Jeju-do","Jeolabuk-do","Jeolanam-do"]}
ajax의 성공시 콜백함수인 success에서는 data.locations로 값을 꺼내 autocomplete의 source를 설정할 수 있다.위와 같은 경우 결과 화면은 다음과 같다.

위와 동일하게 Query문을 통해 입력값에 따라 데이터를 검색하고 Object array로 서버에서 결과값을 가져오는 경우에 대해 가이드한다.Controller에서 List
{"locations":[{"locationId":"0006","locationNm":"Daejeon","localNb":"042"},{"locationId":"0010","locationNm":"Jeju-do","localNb":"064"},{"locationId":"0011","locationNm":"Jeolabuk-do","localNb":"063"},{"locationId":"0012","locationNm":"Jeolanam-do","localNb":"061"}]}
autocomplete의 source에 넘어온 값이 나타나도록 하는 Object는 label, id, value값을 가질 수 있다. 그렇기 때문에 넘어온 request의 값을 success콜백함수에서 source에 나타나도록하는 Object형태로 변환해야 한다.나머지 jQuery구현은 동일하다.
success : function(data) {
response($.map(data.locations, function(item) {
return{
id: item.locationId,
label: item.locationNm,
//value: item.localNb
}));
}
이 때, 자동완성 리스트로 나타나는 값은 label이며, value를 설정해주었을때는 자동완성 리스트에서 값 선택 시 input값에 label값이 아닌 value값이 대입된다.
jQuery에서는 별도로 select box ui함수를 제공하지 않는다.jQuery를 통해 selectbox를 제어하는 방법에 대하여 알아보고 selectbox에 나타나는 리스트를 ajax로 구현하는 방법에 대하여 살펴본다.다루고자 하는 selectbox가 다음과 같다고 가정하자.
<select id="combobox">
<option value="">===locations===</option>
<option value="01">Seoul</option>
<option value="02">Busan</option>
<option value="03">Jeju-do</option>
<option value="04">Incheon</option>
</select>
selectbox에서 선택된 value를 가져오는 방법은 다음과 같다.
var selectedVal = $("#combobox option:selected").val();
selectbox에서 선택된 text(ex:Seoul)를 가져오는 방법은 다음과 같다.
var selectedText= $("#combobox option:selected").text();
selectbox의 리스트에서 선택된 Index를 구하는 방법은 다음과 같다.
var selectedIndex = $("#combobox option").index($("#combobox option:selected"));
selectbox의 리스트에 값을 마지막에 추가하는 방법은 다음과 같다.
$("#combobox").append("<option value="05">Daejeon</option>");
맨 앞에 추가하는 경우는 .prepend()를 쓴다.
selectbox의 리스트의 값들을 교체하는 방법은 다음과 같다.
$("#combobox").html(
"<option value=''>===locations===</option>
<option value='01'>Jeju-do</option>
<option value='02'>Seoul</option>
<option value='03'>Incheon</option>
<option value='04'>Daejeon</option>")
selectbox의 리스트의 값들을 교체하는 방법은 다음과 같다.
$("#combobox").html(
"<option value=''>===locations===</option>
<option value='01'>Jeju-do</option>
<option value='02">Seoul</option>
<option value='03'>Incheon</option>
<option value='04'>Daejeon</option>")
selectbox의 리스트에서 선택된 값을 삭제하는 방법은 다음과 같다.
$("#combobox option:selected").remove();
selectbox에서 값이 선택되었을 때 호출되는 콜백함수는 다음과 같다.
$("#combobox).change(function() {
//기능구현
});
위에서 쓴 selectbox 제어기능과 jQuery ajax함수를 이용하여 다음과 같이 간단히 selectbox를 만들 수 있다.
<JSP에서 콤보박스 구현 예제>
<script type="text/javaScript">
$(function() {
$.ajax({
url : "<c:url value='/simpleCombo.do'/>",
success : function(data) {
loadCombo($("#combobox"), data.locations);
$("#combobox").val("");
}
});
$("#combobox").change(function() {
alert("Selected : " + $("#combobox option:selected").val());
});
});
function loadCombo(target, data) {
var dataArr = [];
var inx = 0;
target.empty();
$(data).each( function() {
dataArr[inx++] = "<option value=" + this.locationId + ">" + this.locationNm + "</option> ";
});
target.append(dataArr);
}
</script>
//... 생략
<select id="combobox">
<option>===locations===</option>
</select>
<서버에서 가져오는 결과값>
{"locations":[{"locationId":"0001","locationNm":"Seoul"},{"locationId":"0002","locationNm":"Busan"},{"locationId":"0003","locationNm":"Daegu"},{"locationId":"0004","locationNm":"Gwangju"},{"locationId":"0005","locationNm":"Incheon"},{"locationId":"0006","locationNm":"Daejeon"},{"locationId":"0007","locationNm":"Ulsan"},{"locationId":"0008","locationNm":"Gyeonggi-do"},{"locationId":"0009","locationNm":"Gangwon-do"},{"locationId":"0010","locationNm":"Jeju-do"},{"locationId":"0011","locationNm":"Jeolabuk-do"},{"locationId":"0012","locationNm":"Jeolanam-do"}]}
ajax함수가 실행되면서 simpleCombo.do를 통해 data를 가져오고 json값이 위와 같을 때 combobox를 구성하는 함수를 구현하여 combobox에 나오는 목록을 나타낼 수 있다. 위의 결과는 다음과 같다.

Tab 기본 구현하기 jQuery UI에서 tab구현은 tabs()를 쓰며 추가 설정은 다음과 같다.
| 설정 | 설명 | 구분 |
|---|---|---|
| active | 활성화될 panel선택 | options |
| event | tab이 활성화되는 이벤트 | options |
| hide | panel이 숨겨질 때 애니메이션 | options |
| show | panel이 나타날 때 애니메이션 | options |
| beforeLoad | Remote tab이 로드되기 전에 실행되기 전에 발생되는 이벤트 함수 | events |
| create | events |
Tab을 구현하는 경우 기본 예제는 다음과 같다.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>jQuery UI Tabs - Default functionality</title>
<link rel="stylesheet" href="//code.jquery.com/ui/1.11.0/themes/smoothness/jquery-ui.css">
<script src="//code.jquery.com/jquery-1.10.2.js"></script>
<script src="//code.jquery.com/ui/1.11.0/jquery-ui.js"></script>
<link rel="stylesheet" href="/resources/demos/style.css">
<script>
$(function() {
$( "#tabs" ).tabs();
});
</script>
</head>
<body>
<div id="tabs">
<ul>
<li><a href="#tabs-1">Nunc tincidunt</a></li>
<li><a href="#tabs-2">Proin dolor</a></li>
<li><a href="#tabs-3">Aenean lacinia</a></li>
</ul>
<div id="tabs-1">
<p>Proin elit arcu, rutrum commodo, vehicula tempus, commodo a, risus. Curabitur nec arcu. Donec sollicitudin mi sit amet mauris. Nam elementum quam ullamcorper ante. Etiam aliquet massa et lorem. Mauris dapibus lacus auctor risus. Aenean tempor ullamcorper leo. Vivamus sed magna quis ligula eleifend adipiscing. Duis orci. Aliquam sodales tortor vitae ipsum. Aliquam nulla. Duis aliquam molestie erat. Ut et mauris vel pede varius sollicitudin. Sed ut dolor nec orci tincidunt interdum. Phasellus ipsum. Nunc tristique tempus lectus.</p>
</div>
<div id="tabs-2">
<p>Morbi tincidunt, dui sit amet facilisis feugiat, odio metus gravida ante, ut pharetra massa metus id nunc. Duis scelerisque molestie turpis. Sed fringilla, massa eget luctus malesuada, metus eros molestie lectus, ut tempus eros massa ut dolor. Aenean aliquet fringilla sem. Suspendisse sed ligula in ligula suscipit aliquam. Praesent in eros vestibulum mi adipiscing adipiscing. Morbi facilisis. Curabitur ornare consequat nunc. Aenean vel metus. Ut posuere viverra nulla. Aliquam erat volutpat. Pellentesque convallis. Maecenas feugiat, tellus pellentesque pretium posuere, felis lorem euismod felis, eu ornare leo nisi vel felis. Mauris consectetur tortor et purus.</p>
</div>
<div id="tabs-3">
<p>Mauris eleifend est et turpis. Duis id erat. Suspendisse potenti. Aliquam vulputate, pede vel vehicula accumsan, mi neque rutrum erat, eu congue orci lorem eget lorem. Vestibulum non ante. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Fusce sodales. Quisque eu urna vel enim commodo pellentesque. Praesent eu risus hendrerit ligula tempus pretium. Curabitur lorem enim, pretium nec, feugiat nec, luctus a, lacus.</p>
<p>Duis cursus. Maecenas ligula eros, blandit nec, pharetra at, semper at, magna. Nullam ac lacus. Nulla facilisi. Praesent viverra justo vitae neque. Praesent blandit adipiscing velit. Suspendisse potenti. Donec mattis, pede vel pharetra blandit, magna ligula faucibus eros, id euismod lacus dolor eget odio. Nam scelerisque. Donec non libero sed nulla mattis commodo. Ut sagittis. Donec nisi lectus, feugiat porttitor, tempor ac, tempor vitae, pede. Aenean vehicula velit eu tellus interdum rutrum. Maecenas commodo. Pellentesque nec elit. Fusce in lacus. Vivamus a libero vitae lectus hendrerit hendrerit.</p>
</div>
</div>
</body>
</html>
위의 결과는 다음과 같다.

ajax로 Tab 구현하기 ajax로 Tab을 구현하기 위해서는 각 Tab에 ajax호출 url을 지정해주기만 하면 된다.
<script src="http://code.jquery.com/jquery-1.10.2.js"></script>
<script src="http://code.jquery.com/ui/1.11.0/jquery-ui.js"></script>
<script type="text/javaScript">
$(function() {
$( "#tabs" ).tabs({
beforeLoad: function( event, ui ) {
ui.jqXHR.error(function() {
ui.panel.html(
"Couldn't load this tab. We'll try to fix this as soon as possible. " +
"If this wouldn't be a demo." );
});
}
});
});
</script>
</head>
<body>
<div id="tabs">
<ul>
<li><a href="${pageContext.request.contextPath }/simpleCombo.do">Tab 1</a></li>
<li><a href="${pageContext.request.contextPath }/tabTwoForm.do">Tab 2</a></li>
<li><a href="${pageContext.request.contextPath }/tabThreeForm.do">Tab 3</a></li>
</ul>
</div>
</body>
위의 경우 첫번째 탭 지정시 simpleCombo.do가 호출되어 panel안에 simpleCombo.do를 통해 호출된 화면이 보여진다.
WebSocket 은 HTTP 환경에서 소켓 통신을 지원하기 위한 Spring 기술이다. Spring 은 기본적으로 WebSocket sub-protocol 로 STOMP 를 사용한다.
(RFC6455는 웹 어플리케이션을 위한 새 기능으로 WebSocket protocol을 정의한다. 서버와 클라이언트간 양방향 통신(full-duplex)을 지원하는데, 이것은 웹을 좀 더 인터랙티브하게 만들기 위해 사용하였던 java applet, XMLHttpRequest, Flash, ActiveX 등의 기술을 대체하기 위한 중요한 기능이 될 수 있다.)
HTTP 는 초기 handshake (protocol upgrade or switch 요청이고 서버가 동의하는 경우 101 응답을 내려 줌)를 위해서만 사용되며, handshake 가 성공하면 HTTP upgrade 요청에 기인하는 TCP 소켓이 open 된 채 서버와 클라이언트간 통신을 처리한다.
WebSocket을 위한 요구사항.
1. WebSocket Fallback Options
WebSocket은 Servlet 3.1에서 제공하기 시작한 기능을 이용하여 구현된 기술로서 모든 브라우저가 WebSocket을 지원하지는 않는다(IE는 10부터 지원). 따라서 WebSocket을 지원하지 않는 브라우저의 경우 해당 기능을 사용할 수 없고, 몇몇 Proxy 에서는 긴 연결상태를 강제로 끊어버리는 등의 오작동이 있을 수 있다. (관련 대안으로 fallback option을 지원하도록 구성된 Spring framework의 SockJS protocol 참고 - Spring 설정을 활성화 시킴으로서 쉽게 Application 에 적용 가능)
2. Messaging Architecture
기존에 구성하던 방식의 개발방법인 REST는 Web application 개발에 있어 많은 URL (noun) 과 몇개의 HTTP method (verbs), 그리고 상태와 무관한 architecture 를 사용한다.
WebSocket application 에서는 초기 HTTP handshake 에서만 하나의 URL 을 사용하고, 그 후의 모든 메시지는 handshake 시 맺어진 TCP 연결을 통해서 전송된다. 이것은 전통적인 messging application (JMS, AMQP) 과 가까운 비동기, event-driven, messaging 아키텍처를 사용하는 것이다.
Spring Framework 4.0 에서는 spring-messging 모듈을 통해 기능을 제공하는데, 이 모듈은 스프링 통합 프로젝트에서 사용하는 Message, MessageChannel, MessageHandler 와 같은 추상화된 개념을 제공하여 messaging archtecture 의 근간이 된다. 이 모듈은 Spring MVC annotaion 기반의 programming model 과 유사하게 작성할 수 있도록 몇 가지 annotation 을 포함하고 있다.
3. Sub-Protocol Support in WebSocket
WebSocket 은 messaging architecture 를 함축하지만 특정 messaging protocol 사용을 강제하지 않는다. 이것은 매우 얇은 layer 이며 단순히 TCP 위에서 일련의 byte 스트림들을 메시지로 변환하는 것에 지나지 않는다. 메시지의 의미 해석은 Application 에 맡겨둔 채 말이다.
HTTP (이것은 application level protocol 이다.) 와 달리 WebSocket 의 protocol 은 단순화 하여 어떻게 처리할지 어디로 route 할지에 대한 충분한 정보가 제공되지 않는 저수준으로 제공된다. 이러한 이유로 WebSocket RFC 에서는 sub-protocol 들의 사용을 정의하였다. handshake 사용 중 클라이언트와 서버는 Sec-WebSocket-Protocol 헤더를 사용할 수 있고 이를 통해 서로 통신할 sub protocol 을 합의한다. WebSocket 은 sub-protocol 을 강제하지 않기에 필수적인 것은 아니지만 이 경우 client/server 는 서로 message 포맷에 대해 합의되어야 한다.
Spring 은 STOMP 지원을 제공하며 이는 HTTP 와 유사한 형태로 script 언어에서 사용을 위해 만들어진 것이다. STOMP 는 널리 지원되고 WebSocket 사용에 적합하다.
4. WebSocket 을 사용하여야 하는가?
WebSocket 은 서버와 클라이언트가 적은 지연에 매우 빈번하게 이벤트를 교환해야 할 경우에 적합하다. 이는 finance, game, collaboration 등의 application 일 수 있다. 이들은 모두 time delay 에 민감하고 매우 빈번하게 많은 종류의 메시지를 교환해야 한다.
social, news feed 와 같은 경우는 단순 polling 으로 충분하다. 이들은 latency 가 중요한 요소가 아니다. latency 가 중요하더라도 message 의 크기가 작다면 (network failure check 같은) long polling 이 충분한 대안이 될 수 있다.
low latency 와 high frequency 가 중요한 경우에는 WebSocket 이 적합하다. 하지만 이러한 경우에도 모든 client-server 통신이 WebSocket 을 이용해야 하는 지는 application 에 따라 다를 수 있다. 최적의 경우를 생각하여 client 가 사용할 수 있는 대안으로서 WebSocket 과 REST API 를 모두 제공해야 할 수도 있다. 이 경우 REST 호출을 통해 특정 메시지를 WebSocket 클라이언트들에게 모두 전달될 필요가 있을 수도 있다.
Spring Framework 는 @Controller, @RestController 클래스에 HTTP 핸들링 메소드와 WebSocket 핸들링 메소드를 사용할 수 있다. 게다가 Spring MVC 요청 핸들링 메소드는 모든 WebSocket 클라이언트에 메시지를 전달하거나 특정 사용자에게만 전달하는 것을 쉽게 제공한다.
Spring Framework 는 다양한 WebSocket Engine 에 적합하게 설계되었다. 예를 들어 Tomcat (7.0.47+) or GlassFish (4.0+), WildFly (8.0+) JSR-356 런타임상에서 구동하거나 Jetty (9.1+) 에서와 같이 native WebSocket 지원 환경에서 구동할 수 있다.
직접적인 WebSocket API 를 application 개발에 사용하는 것은 매우 저수준의 행위까지 설계해야 한다. 메시지 포맷에 대한 것 또는 annotation 을 통한 message 라우팅 같은 것을 지원하도록 application 에서는 sub-protocol 을 사용하는 것이 좋으며, 이러한 맥락에서 Spring 은 STOMP over WebSocket 을 지원한다.
1. WebSocketHandler 설정
Spring 은 WebSocketHandler 를 구현함으로 WebSocket 서버를 만드는 것을 지원한다. WebSocketHandler 는 TextWebSocketHandler 나 BinaryWebSocketHandler 로 세분화 되어 있다.
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.TextMessage;
public class MyHandler extends TextWebSocketHandler {
@Override
public void handleTextMessage(WebSocketSession session, TextMessage message) {
// ...
}
}
특정 URL 에 WebSocketHandler 를 설정하는 Java-Config 설정
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(myHandler(), "/myHandler");
}
@Bean
public WebSocketHandler myHandler() {
return new MyHandler();
}
}
동일 설정을 위한 XML Config 설정
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:websocket="http://www.springframework.org/schema/websocket"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/websocket
http://www.springframework.org/schema/websocket/spring-websocket-4.0.xsd">
<websocket:handlers>
<websocket:mapping path="/myHandler" handler="myHandler"/>
</websocket:handlers>
<bean id="myHandler" class="org.springframework.samples.MyHandler"/>
</beans>
위의 설정은 Spring 의 DispatcherServlet 을 사용하는 설정이다. 하지만 Spring 은 다른 Web 개발 환경에서 WebSocketHandler 를 사용할 수 있도록 WebSocketHttpRequestHandler 를 지원한다.
2. WebSocket Handshaking 커스터마이즈
초기 WebSockethandshake 를 커스터마이징 하는 가장 손쉬운 방법은 HandshakeInterceptor 를 사용하는 것인데 이것은 handshake 하는 것에 대한 before 와 after 처리를 기술할 수 있다. 이것은 handshake 를 준비하기 위해 사용되거나 WebSocketSession 에서 특정 attribute 를 이용할 수 있도록 하는데 사용된다. 아래는 Spring 에서 제공되는 interceptor 로 http 세션 attribute 를 WebSocketSession 에 전달해 주는 일을 한다.
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(new MyHandler(), "/myHandler")
.addInterceptors(new HttpSessionHandshakeInterceptor());
}
}
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:websocket="http://www.springframework.org/schema/websocket"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/websocket
http://www.springframework.org/schema/websocket/spring-websocket-4.0.xsd">
<websocket:handlers>
<websocket:mapping path="/myHandler" handler="myHandler"/>
<websocket:handshake-interceptors>
<bean class="org.springframework.web.socket.server.support.HttpSessionHandshakeInterceptor"/>
</websocket:handshake-interceptors>
</websocket:handlers>
<bean id="myHandler" class="org.springframework.samples.MyHandler"/>
</beans>
좀 더 고급 옵션을 적용하려면 DefaultHandshakeHandler 를 확장하여야 한다. 이것은 validating, client origin, sub-protocol 협의 및 기타 여러가지를 포함하고 있다. application 은 특정 WebSocket server engine 이나 아직 지원하지 않는 버전에 대한 지원을 위해서 커스텀 RequestUpgradeStrategy 를 설정할 필요가 있다.
3. WebSocketHandler Decoration
Spring 은 WebSocketHandlerDecorator 기본 클래스를 제공한다. 이것은 WebSocketHandler 에 추가적인 행위를 decorate 하는데 사용된다. Logging, Exception Handling WebSocketHandlerDecorator 구현이 Java-config 나 XML config 를 통해서 기본적으로 제공되고 있다. ExceptionWebSocketHandlerDecorator 는 WebSocketHandler 에서 발생하는 처리되지 않은 모든 예외를 catch 하여 Server error 를 나타내기 위해 1011 status code 로 응답한다.
4. 배포 고려사항
Spring WebSocket API 는 Spring MVC application 과 통합이 쉽다. 그것은 Dispatcher Servlet 이 HTTP WebSocket handshake 뿐만 아니라 다른 HTTP 요청도 서비스 하기 때문이다. WebSocketHttpRequestHandler 를 사용하면 다른 HTTP 처리 시나리오 (Spring MVC 이외의 다른 Web Framework 와 같은) 와도 쉽게 결합할 수 있다. 그러나 JSR-356 runtime 에 특별한 고려사항이 있다.
Java WebSocket API (JSR-356) 는 두가지 배포 메커니즘을 제공한다. 첫번째는 시작 시 Servlet container classpath scan (Servlet 3 의 기능) 에 관한 것이고 다른 것은 Servlet container 초기화에서 사용하기 위한 registration API 에 관한 것이다. 이것 중 어느 것도 모든 HTTP 요청을 처리하는 단일 front controller 를 사용하지 못한다. 즉 DispatcherServlet 은 원칙적으로 WebSocket handshake 와 다른 모든 HTTP 요청에 대해서 사용할 수 없다.
이것은 JSR-356 이 가지는 매우 중요한 한계인데 Spring 의 WebSocket 지원은 JSR-356 runtime 환경에서도 서버 특화의 RequestUpgradeStrategy 를 제공함으로 이를 지원하고 있다. 현재 Spring 은 Tomcat 7.0.47+, Jetty 9.1+, GlassFish4.0+, WildFly8.0+ 을 지원한다. 추가적인 지원은 더 많은 WebSocket runtime 을 이용할 수 있을 때 추가될 것이다.
두 번째 고려사항은 JSR-356 지원의 Servlet container 가 SCI scan 을 수행하도록 하고 있어 application 구동을 느리게 (혹은 어떤 특정 상황에서는 매우 느리게) 할 수 있다는 것이다. 만약 JSR-356 지원 Servlet container version 업그레이드시 명백한 영향이 목격된다면, 선택적으로 web fragments (SCI scanning) 을 활성 및 비활성할 수 있다. (이는 web.xml 의 absolute-ordering 을 사용토록 한다.) web fragment 순서를 명확히 하도록 web.xml 에서 absolute-ordering 을 사용 토록 한다. (이는 scanning 을 사용치 않도록 하는 방법이다.)
5. WebSocket 엔진 설정하기
각각의 기반이 되는 WebSocket engine 들은 message buffer size 나 idle timeout 등의 runtime 특성을 조절할 수 있는 configuration properties 를 가지고 있다. 이는 Tomcat, WildFly, Glassfish 에서ServletServerContainerFactoryBean 을 WebSocket java config 에 추가하여 설정할 수 있다.
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Bean
public ServletServerContainerFactoryBean createWebSocketContainer() {
ServletServerContainerFactoryBean container = new ServletServerContainerFactoryBean();
container.setMaxTextMessageBufferSize(8192);
container.setMaxBinaryMessageBufferSize(8192);
return container;
}
}
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:websocket="http://www.springframework.org/schema/websocket"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/websocket
http://www.springframework.org/schema/websocket/spring-websocket.xsd">
<bean class="org.springframework...ServletServerContainerFactoryBean">
<property name="maxTextMessageBufferSize" value="8192"/>
<property name="maxBinaryMessageBufferSize" value="8192"/>
</bean>
</beans>
client 측 WebSocket 설정을 위해서는 WebSocketContainerFactoryBean(XML) 이나 ContainerProvider.getWebSocketContainer() 를 이용한다.
Jetty 를 사용하는 경우에는 미리 설정된 WebSocketServerFactory 를 필요로 하고 이를 WebSocket java config 를 통해서 Spring 의 DefaultHandshakeHandler 에 추가한다.
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(echoWebSocketHandler(),
"/echo").setHandshakeHandler(handshakeHandler());
}
@Bean
public DefaultHandshakeHandler handshakeHandler() {
WebSocketPolicy policy = new WebSocketPolicy(WebSocketBehavior.SERVER);
policy.setInputBufferSize(8192);
policy.setIdleTimeout(600000);
return new DefaultHandshakeHandler(
new JettyRequestUpgradeStrategy(new WebSocketServerFactory(policy)));
}
}
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:websocket="http://www.springframework.org/schema/websocket"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/websocket
http://www.springframework.org/schema/websocket/spring-websocket.xsd">
<websocket:handlers>
<websocket:mapping path="/echo" handler="echoHandler"/>
<websocket:handshake-handler ref="handshakeHandler"/>
</websocket:handlers>
<bean id="handshakeHandler" class="org.springframework...DefaultHandshakeHandler">
<constructor-arg ref="upgradeStrategy"/>
</bean>
<bean id="upgradeStrategy" class="org.springframework...JettyRequestUpgradeStrategy">
<constructor-arg ref="serverFactory"/>
</bean>
<bean id="serverFactory" class="org.eclipse.jetty...WebSocketServerFactory">
<constructor-arg>
<bean class="org.eclipse.jetty...WebSocketPolicy">
<constructor-arg value="SERVER"/>
<property name="inputBufferSize" value="8092"/>
<property name="idleTimeout" value="600000"/>
</bean>
</constructor-arg>
</bean>
</beans>
WebSocket protocol 은 content 를 정의하지 않은 채 2 가지 유형 (text, binary) 의 메시지로 분류했다. content 를 정의하지 않은 대신 client 와 서버는 sub-protocol (content 를 정의하는 고수준의 protocol) 을 사용하는 것을 합의해야 할 수도 있다. sub-protocol 을 사용하는 것은 option 이지만 client 와 server 모두 메시지를 어떻게 해석해야 할지를 이해하는 것이 필요하다.
STOMP 는 Ruby, Python, Perl 과 같은 스크립트 언어를 위해 고안된 단순한 메시징 프로토콜이다. 그것은 메시징 프로토콜에서 일반적으로 사용되는 패턴들의 일부를 제공한다. STOMP 는 TCP 나 WebSocket 과 같은 신뢰성있는 양방향 streaming network protocol 상에 사용될 수 있다.
STOMP 는 HTTP 에 모델링된 frame 기반 프로토콜이다. 다음은 frame 의 구조이다.
COMMAND
header1:value1
header2:value2
Body^@
클라이언트는 메시지를 보내기 위해 SEND 명령을 사용하거나 수신 메시지에 관심을 표현하기 위해 SUBSCRIBE 명령을 사용할 수 있다. 이런 명령어들은 “destination” 헤더를 요구하는데 어디에 메시지를 전송할 지 혹은 어디에서 메시지를 구독할지를 나타낸다.
다음은 stock shares 를 구매하기 위한 요청 전송의 예이다.
SEND
destination:/queue/trade
content-type:application/json
content-length:44
{"action":"BUY","ticker":"MMM","shares",44}^@
다음은 stock quotes 를 얻기위한 클라이언트 구독의 예이다.
SUBSCRIBE
id:sub-1
destination:/topic/price.stock.*
^@
destination 의 의미는 STOMP spec 에서 투명하게 남겨두었다. 그것은 어떤 문자열이든 될 수 있고 그 의미나 문법은 온전히 STOMP 서버에 맡겨진다. 그러나 일반적으로 다음의 규칙을 사용하곤 한다.
“topic/..” - publish-subscribe (one to many) “queue/” - point-to-point (one to one)
STOMP 서버는 모든 구독자에게 message 를 broadcasting 하기 위해 MESSAGE 명령을 사용할 수 있다. 다음은 stock quote 를 구독자에서 전달하는 예이다.
MESSAGE
message-id:nxahklf6-1
subscription:sub-1
destination:/topic/price.stock.MMM
{"ticker":"MMM","price":129.45}^@
서버는 불분명한 메시지를 전송할 수 없음을 알아야 한다. 즉 서버의 모든 메시지는 특정 클라이언트 구독에 응답하여야 하고 서버 메시지의 “subscription-id” 헤더는 클라이언트 구독의 “id” 헤더와 일치하여야 한다.
지금까지 STOMP 의 가장 기본적인 이해를 위한 것이다. 상세한 것은 specification 을 통해 살펴볼 수 있다.
다음은 STOMP over WebSocket 사용 application 의 장점을 요약한 것이다.
순수 WebSocket 과 비교하여 STOMP 사용의 가장 중요한 요소는 Spring Framework 가 마치 SpringMVC 가 HTTP 에 프로그래밍 모델을 제공하는 것처럼 application 수준의 사용을 위한 프로그래밍 모델을 제공한다는 것이다.
Spring Framework 는 spring-messaging 와 spring-websocket 모듈을 통해 STOMP over WebSocket 사용을 제공한다.
다음은 SockJS fallback option 을 사용하는 STOMP WebSocket endpoint 설정의 예이다. endpoint 는 /app/portfolio URL 경로에 클라이언트가 접속할 수 있다.
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.setApplicationDestinationPrefixes("/app")
.enableSimpleBroker("/queue", "/topic");
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/portfolio").withSockJS();
}
// ...
}
동일 XML 설정은 다음과 같다.
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:websocket="http://www.springframework.org/schema/websocket"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/websocket
http://www.springframework.org/schema/websocket/spring-websocket-4.0.xsd">
<websocket:message-broker application-destination-prefix="/app">
<websocket:stomp-endpoint path="/portfolio">
<websocket:sockjs/>
</websocket:stomp-endpoint>
<websocket:simple-broker prefix="/queue, /topic"/>
...
</websocket:message-broker>
</beans>
브라우저 측에서는 stomp.js 나 sockjs-client 를 사용하여 접속한다.
var socket = new SockJS("/spring-websocket-portfolio/portfolio");
var stompClient = Stomp.over(socket);
stompClient.connect({}, function(frame) {
}
SockJS 없는 WebSocket 사용은 다음과 같다.
var socket = new WebSocket("/spring-websocket-portfolio/portfolio");
var stompClient = Stomp.over(socket);
stompClient.connect({}, function(frame) {
}
위의 stomp 클라이언트는 login 과 passcode 헤더를 정의할 필요가 없다. 제공되더라도 무시되거나 서버측에서 재정의 될 것이다. 인증에 대해서는 Section 20.4.8, “Connections To Full-Featured Broker” 와 Section 20.4.9, “Authentication” 를 참고하라
STOMP endpoint 가 설정될 때, spring application 은 연결된 클라이언트들에게 마치 STOMP broker 인 것처럼 동작한다. 들어오는 메시지를 처리하고 다시 메시지를 전송한다. 이 장에서는 Application 내에서 message flow 가 어떠한지에 대한 개략적인 개요를 제공한다.
spring-messaging 모듈은 Spring Integration 프로젝트에 기반한 다양한 추상화를 포함하고 있으며, messging application 의 building block 으로 사용하도록 의도되었다.
제공되는 STOMP over WebSocket 설정 (Java,XML 모두) 은 다음의 3 가지 channel 들을 포함하여 실제적인 message flow 를 만들어내는데 사용된다.
“clientInboundChannel” 의 메시지는 처리 (요청 실행과 같은) 를 위해 annotated method 로 흐르거나 broker (구동과 같은) 에 포워딩 될 수 있다. STOMP destination 은 단순한 prefix 기반 라우팅을 위해 사용되어진다. 예를 들어 ”/app” prefix 는 annotated method 로 라우팅하고 ”/topic” 이나 ”/queue” 는 broker 에 라우팅되는 것이다.
메지시 처리 annotated method 가 return type 을 갖을 때는 그 return value 는 Spring Message 의 payload 로서 “brokerChannel” 에 전송된다. 그러면 broker 는 메시지를 client 들에게 broadcasting 한다. 메시지를 destination 에 전송하는 것은 messaging template 의 도움으로 application 내 어디에서나 수행될 수 있다. 예를 들어, HTTP POST 처리 메소드는 메시지를 연결된 클라이언트들에게 broadcast 할 수 있는 것이다. 또는 service component 는 주기적으로 stock quotes 를 broadcast 할 수 있다.
다음은 메시지 흐름을 보여주는 단순한 예이다.
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/portfolio");
}
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.setApplicationDestinationPrefixes("/app");
registry.enableSimpleBroker("/topic/");
}
}
@Controller
public class GreetingController {
@MessageMapping("/greeting") {
public String handle(String greeting) {
return "[" + getTimestamp() + ": " + greeting;
}
}
다음은 위 예제의 메시지 흐름에 대한 설명이다.
다음 장에서 argument 와 return value 의 종류를 포함한 annotated method 의 상세한 내용을 제공한다.
@MessageMapping annotation 은 @Controller 와 @RestController annotated class 의 메소드에 적용할 수 있다. 이것은 메소드를 path-like message destination 에 매핑하기 위해 사용될 수 있다. 이것은 또한 controller 내의 모든 annotated method 들과 공유되는 매핑을 표현하기 위해 type-level @MessageMapping 과 결합할 수 있다.
Destination mapping 은 Ant-style pattern (e.g. ”/foo*”, ”/foo/**”) 과 template variable (e.g. ”/foo/{id}”, 이것은 @DestinationVariable 메소드 인자를 통해 접근될 수 있다.) 을 포함할 수 있다. 이처럼 SpringMVC 사용자 친화적인데, 실질적으로 SpringMVC 가 그러하듯 AntPathMatcher 는 pattern 기반의 destination mapping 과 template variable 추출에 사용된다.
@MessageMapping 메소드에서 다음의 method 인자가 지원된다.
@MessageMapping 메소드의 return value 는 org.springframework.messaging.converter.MessageConverter 를 통해 변환되고 새로운 메시지의 body 로 사용된다. 이 새로운 메시지는 기본적으로 “brokerChannel” 에 client 메시지와 같은 destination 이지만 기본값 ”/topic” prefix 를 사용하여 전달된다. @SendTo message level annotation 은 다른 destination 을 지정하고자 할 때 사용될 수 있다.
@SubscribeMapping annotation 은 @Controller 메소드에 구독 요청을 매핑하기 위해 사용될 수 있다. 이것은 method level 에 지원되지만, type level 의 @MessageMapping annotation 과 결합하여 같은 controller 내의 모든 message handling method 들과의 공유 매핑을 표현할 수 있다.
기본적으로 @SubscribeMapping 메소드의 return value 는 broker 에 전달되는 것이 아니라 바로 연결된 클라이언트들에 메시지로서 전달된다. 이것은 request-reply message 교환에 유용하다. 예를 들어, application UI 가 초기화 될 때 application data 를 가져오는 것이다. 대안으로서 @SubscribeMapping 메소드가 @SendTo 가 적용되는 경우 결과 message 는 지정된 대상 destination 을 사용하여 “brokerChannel” 에 전달된다.
Application 의 어떤 부분에서라도 접속된 client 들에게 메시지를 보내고자 할 때 어떻게 해야 할까? 어떠한 application component 라도 “brokerChannel” 에 메시지를 전송할 수 있다. 이를 위한 가장 쉬운 방법은 SimpMessagingTemplate 을 주입받아서 메시지 전송에 사용하는 것이다. 이것은 다음의 예제를 통해 확인할 수 있다.
@Controller
public class GreetingController {
private SimpMessagingTemplate template;
@Autowired
public GreetingController(SimpMessagingTemplate template) {
this.template = template;
}
@RequestMapping(value="/greetings", method=POST)
public void greet(String greeting) {
String text = "[" + getTimestamp() + "]:" + greeting;
this.template.convertAndSend("/topic/greetings", text);
}
}
기본 제공되는 단순한 message broker 는 클라이언트로부터의 구독 요청을 처리하고 그것을 메모리에 저장하고 일치하는 destination 을 가지는 연결된 client 들에 메시지를 brodcasting 한다. broker 는 Ant-style destination pattern 구독을 포함하는 동시에 path-like destination 을 지원한다.
Simple broker 가 초기 시작을 위해서는 훌륭하지만 STOMP 명령의 subset 들만을 지원하고 (e.g. no acks, receipts, etc) 단순 메시지 전송 loop 에 의존하며, clustering 에 적합하지 않다. 대신 application 은 full-featured message broker 를 사용하도록 업그레이드 할 수 있다.
적합한 message broker 선택 ( RabbitMQ, ActiveMQ, others) 을 위해 STOMP 문서를 확인하라. 그것을 설치하고 STOMP 지원을 활성화하여 broker 를 구동하라. 그 다음 Spring configuration 에서 simple broker 대신 STOMP broker relay 를 활성화 하라.
다음은 full-featured broker 를 활성화하는 설정예제이다.
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/portfolio").withSockJS();
}
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableStompBrokerRelay("/topic/", "/queue/");
registry.setApplicationDestinationPrefixes("/app");
}
}
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:websocket="http://www.springframework.org/schema/websocket"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/websocket
http://www.springframework.org/schema/websocket/spring-websocket-4.0.xsd">
<websocket:message-broker application-destination-prefix="/app">
<websocket:stomp-endpoint path="/portfolio" />
<websocket:sockjs/>
</websocket:stomp-endpoint>
<websocket:stomp-broker-relay prefix="/topic,/queue" />
</websocket:message-broker>
</beans>
위의 설정에서 “STOMP broker relay” 는 Spring MessageHandler 인데 이것은 외부 broker 에 메시지들을 forwarding 함으로써 메시지들을 처리한다. 그렇게 하기 위해 broker 에 TCP 연결을 맺고 모든 메시지를 broker 에 forwarding 하고 broker 로 부터 수신된 메시지를 WebSocket session 을 통해 client 에 역 forwarding (되돌려준다.) 한다. 본질적으로 그것은 마치 양방향으로 message 를 forwarding 하는 “relay” 처럼 동작한다.
org.projectreactor:reactor-net 의존성을 추가하여 TCP connection 관리를 할 수 있다.
게다가 application components (e.g. HTTP request handling method, business service, etc) 는 또한 메시지를 broker relay 에 전송할 수 있다. Section 20.4.5, “Sending Messages” 에서 subscribed WebSocket 클라이언트들에 메시지를 broadcast 하는 것이 설명되어 있다.
사실상 broker relay 는 강력하고 scalabale 한 message brodcasting 을 활성화 한다.
STOMP broker relay 는 broker 에 대한 단일 “system” TCP connection 을 유지한다. 이 connection 은 receiving message 가 아닌 server 측 application 에 기인하는 message 들만을 위해 사용된다. 이러한 접속을 위해 STOMP credentials 을 설정할 수 있다. (STOMP frame login 과 passcode 헤더) 이것은 Java config 와 XML 모두에서 systemLogin/systemPasscode 속성으로 노출된다. (기본값은 guest/guest 이다.)
STOMP broker relay 는 또한 모든 접속된 WebSocket client 를 위해 별도의 TCP 연결을 생성한다. 클라이언트를 대신해 모든 TCP 연결을 위해 STOMP credetial 을 설정할 수 있다. 이것은 Java config 와 XML 모두에서 clientLogin/clientPasscode 속성으로 노출된다. (기본값은 guest/guest 이다.)
STOMP broker relay 는 또한 “system” TCP connection 상에서 message broker 로부터의/로 hearbeat 을 전송하고 수신한다. heartbeat 전송/수신의 interval 을 설정할 수 있는데 기본값은 10 초이다. 만약 broker 에 대한 연결이 끊어지면 broker relay 는 재접속 시도를 성공할 때까지 5 초마다 계속한다.
Spring Bean 은 ApplicationListener<BrokerAvailabilityEvent> 를 구현할 수 있는데 이를 통해 broker 에 대한 “system” connection 이 끊기고 재접속될 때 알림을 받을 수 있다. 예를 들어, stock quote 를 broadcasting 하는 Stock Quote service 는 어떠한 active “system” connection 이 없을 때 메시지 전송시도를 중단할 수 있다.
STOMP broker relay 는 virtualHost 속성으로 설정될 수 있다. 이 속성의 값은 모든 CONNECT frame 의 host header 에 설정될 것이고 예를 들어 TCP connection 이 맺어진 실제 host 가 cloud-based STOMP service 를 제공하는 host 와 다른 cloud 환경에서 유용할 수 있다.
WebSocket-style application 에서 누가 메시지를 보냈는지 아는것은 때때로 유용하다. 그래서 몇가지 인증 형태가 사용자 identity 를 형성하고 그것을 현재 session 에 연결하는데 필요하다.
Web application 은 이미 HTTP 기반의 인증을 사용한다. 예를 들어, Spring Security 는 보통 application 의 HTTP URL 을 보호할 수 있다. WebSocket session 이 HTTP handshake 로 시작하기 때문에 STOMP/WebSocket 에 매핑된 URL 은 이미 보호되고 인증을 요구한다는 것을 의미한다. 게다가 WebSocket 연결을 시작하는 page 는 그 스스로 거의 보호되는 것이고 실제 handshake 시까지 그 사용자는 인증되어져야 한다.
WebSocket hanshake 가 형성되고 새로운 WebSocket session 이 생성될 때 Spring WebSocket 지원은 자동적으로 HTTP request 로 부터 WebSocket session 으로 java.security.Principal 을 전달한다. 이후 WebSocket session 상의 application 을 통한 모든 message flow 는 사용자 정보가 포함되어진다. 그것은 message 의 header 에 존재한다. Controller 메소드는 javax.security.Principal 타입의 메소드 인자를 추가함으로 현재 사용자에 접근할 수 있다.
비록 STOMP CONNECT frame 이 인증을 위한 “login” 과 “passcode” 헤더를 가지고 있다하더라도, Spring 의 STOMP WebSocket 지원은 그것들을 무시하고 사용자가 HTTP 를 통해 기 인증되었기를 기대한다.
어떤 경우에는 사용자가 공식적으로 인증되지 않았을 때일지라도 WebSocket session 에 identity 를 부여하는것이 유용할 수도 있다. 예를 들면, mobile app 은 익명의 사용자에 어떤 identity (아마도 지리적 위치에 기반하여) 를 부여하기도 한다. 이때에 사용자 정의 handshake handler 가 사용될 수 있다. (Section 20.2.4, “Deployment Considerations” 에서 예제를 확인하라)
Application 은 특정 사용자를 지정하여 메시지를 전송할 수 있다. 연결된 사용자가 메시지를 수신하기 위해 그들은 인증되어져야 한다. 그래야 그들의 session 이 실제 user name 과 연결되기 때문이다. 앞선 section 에서 인증에 대해 확인하라
Spring 의 STOMP 지원은 /user/ 접두어로 destination 을 식별한다. 예를 들어, 클라이언트가 /user/position-updates destination 에 subscribe 할 수 있다. 이 destination 은 UserDestinationMessageHandler 에 의해 처리되고 사용자 session 에 unique 한 destination 으로 변환된다. (e.g. /user/position-updates-123) 이것은 일반적으로 명명된 destination 에 subscribing 편의를 제공하는 동시에 /user/position-updates 에 subscribe 하는 어떤 다른 사용자와 충돌하지 않음을 보장한다.
전송 측에서 메시지는 /user/{username}/position-updates 와 같은 destination 에 전송될 수 있다. 그러면 UserDestinationMessageHandler 에 의해 지정한 사용자 명에 속한 unique destination 과 같도록 번역될 것이다.
이것은 application 내의 어떤 component 도 name 과 일반적인 destination 만 알면 특정 사용자에 메시지를 전송할 수 있게 한다. 이것이 외부 message broker 와 같이 사용될 때 사용자 session 이 넘치면 모든 unique 한 사용자 queue 가 삭제되도록 하기 위해 inactive queue 를 다루는 방법에 대해 broker 문서를 확인하라. 예를 들어 RabbitMQ 는 /exchange/amq.direct/position-updates 와 같은 destination 이 사용될 때 auto-delete queue 를 생성한다. 클라이언트가 /user/exchange/amq.direct/position-updates 에 subscrbe 하는것이 그러한 경우이다. ActiveMQ 는 inactive destination 을 제거하기 위한 configuration options 이 있다.
STOMP messaging 지원은 다음의 ApplicationContext 이벤트를 발생시킨다. 이러한 이벤트들중 하나 혹은 그 이상을 처리하기 위해 Spring 관리 component 는 ApplicationListener 를 구현할 수 있다. 이벤트는 다음과 같다.
BrokerAvailabilityEvent — broker 가 available/inavailable 될 때 나타난다. “simple” broker 는 시작시 즉시 available 되고 application 구동중 유지되지만 STOMP “broker relay” 는 만약 broker 가 재시작되면 full featured broker 에 대한 연결을 잃을 지도 모른다. broker relay 는 reconnect logic 을 가지고 있고 broker 에 대한 “system” connection 을 broker 가 돌아오면 재설정 할 것이다. 그 결과 이 event 는 상태가 connected 에서 disconnected 가 될 때나 그 반대일때마다 발생한다. SimpMessagingTemplate 을 사용하는 Components 는 이 이벤트에 subscribe 하여야 하며 broker 가 이용불가일 때 메시지를 전송하지 말아야 한다. 어떤 경우에서는 메시지를 전송할 때 MessageDeliveryException 을 처리하도록 준비되어야 한다. SessionConnectEvent — 새로운 클라이언트 session 을 나타내는 STOMP CONNECT 가 수신될 때 나타난다. 이 이벤트는 session id , user information 및 client 가 보냈을 지 모르는 어떤 사용자 정의 header 를 포함한다. 이것은 client sessions 을 추적하는데 유용하다. 이 이벤트에 subscribe 하는 Components 는 SimpMessageHeaderAccessor 나 StompMessageHeaderAccessor 를 사용하여 메시지를 wrapping 할 수 있다. SessionConnectedEvent — broker 가 SessionConnectEvent 이후 CONNECT 에 대한 응답으로 STOMP CONNECTED frame 을 전송했을 때 짧게 발생한다. 이 시점에 STOMP session 은 완전히 연결되었음으로 간주될 수 있다. SessionDisconnectEvent — STOMP session 이 끝났을 때 발생한다. DISCONNECT 는 client 로 부터 전송되거나 WebSocket session 이 closed 될 때 자동적으로 발생되어질 수 있다. 어떤 경우에 이 이벤트는 세션당 한번 이상 발생할수도 있다. Components 는 복수의 disconnect events 에 대해 idempotent(멱등) 해야 한다. full-featured broker 를 사용할 때, STOMP “broker relay” 는 자동적으로 broker 가 일시적으로 이용불가한 “system” connection 을 재접속 한다. 그러나 Client connections 는 자동적으로 재접속 되지 않는다. heartbeats 이 활성화 되어 있다면, client 는 일반적으로 10초 안에 broker 가 응답하지 않음을 알게 될 것이다. Clients 는 스스로 reconnect logic 을 구현해야할 필요가 있다.
성능에 대한 은총알은 없다. 많은 요인 (메시지 size, volume, application 메소드가 blocking 을 요하는 작업을 수행하는지 여부, network 속도 같은 외부요소 및 기타등등) 이 영향을 끼친다. 이 section 의 목적은 scaling 에 대한 생각과 더불어 이용가능한 configuration options 의 개요를 제공하는 것이다.
messaging application 에선 messages 는 thread pool 에 기반한 비동기 수행을 위해 channels 을 통해 전달된다. 이런 application 을 설정하는 것은 channels 과 messages flow 에 대한 지식이 요구된다. 그래서 Section 20.4.3, “Flow of Messages” 를 리뷰해보기 바란다.
시작하기 좋은 위치는 “clientInboundChannel” 과 “clientOutboundChannel” 를 지지하는 thread pools 을 설정하는 것이다. 기본적으로 이 둘은 사용가능한 processor 수의 2배로 설정된다.
막약 annotated method 에서 메시지 처리가 주로 CPU 소비에 기인하면 “clientInboundChannel” 의 thread 수는 processor 수에 가깝게 유지되어야 한다. 만약 작업이 IO 소비에 기인하고 database 나 다른 external system 에 blocking 을 요한다면 thread pool size 는 증가되어야 할 필요가 있다.
ThreadPoolExecutor 는 3 가지 중요한 property 를 갖는다. 그것은 core 와 max thread pool size 와 이용가능한 thread 가 없을 때 작업을 저장하기 위한 queue capacity 이다.
일반적인 혼동이 오는 지점은 core pool size (e.g. 10) 와 max pool size (e.g. 20) 설정이 10 에서 20 threads 를 가지는 thread pool 로 나타나는 것이다. 사실 capacity 가 그 기본값인 Integer.MAX_VALUE 인채로 남겨두면 thread pool 은 모든 추가적인 작업이 queue 에 쌓일 것이기 때문에 결코 core pool size 위로 증가하지 않을 것이다.
이러한 properties 들이 어떻게 작용하고 다양한 queuing strategies 를 이해하기 위해 ThreadPoolExecutor 의 Javadoc 을 확인하라.
“clientOutboundChannel” 측에서 thread pool 은 온전히 WebSocket clients 에 메시지를 전송하는 것과 관련있다. 만약 clients 가 빠른 network 에 있다면 스레드 수는 이용가능한 processors 수에 가깝게 유지되어야 한다. 만약 그들이 느리거나 low bandwidth 상에 있다면 그들은 메시지 소비를 위해 더 오래 점유할 것이고 thread pool 에 부담을 줄 것이다. 그래서 thread pool size 증가가 필요할 수도 있을 것이다.
“clientInboundChannel” 에 대한 부하는 예상가능하다 — 결국 그것은 application 이 무엇을 하는지에 기인한다.— ”clientOutboundChannel” 을 어떻게 설정해야 하는지는 application 제어를 넘어서는 요인에 기인하기 때문에 좀더 어렵다. 이러한 이유로 메시지 전송과 관련한 두가지 추가적인 properties 가 있다. 그것은 “sendTimeLimit” 와 “sendBufferSizeLimit” 이다. 이것은 얼마나 오래 그 전송이 허락되는가와 얼마나 많은 데이터가 client 에 메시지 전송시 buffer 되는지를 설정하는데 사용된다.
일반적인 생각은 어떤 주어진 시간에 단 하나의 thread 만이 client 에 메시지 전송을 위해 사용되어져야 한다는 것이다. 모든 추가적인 메시지는 동시에 버퍼링 되고 이러한 properties 가 얼마나 오래 메시지 전송에 점유가 허락되는가와 얼마나 많은 데이터가 동시에 버퍼링 되는가에 사용할 수 있다. 중요한 추가적인 세부사항은 XML schema 의 Javadoc 을 참고하라.
다음은 설정 예이다.
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void configureWebSocketTransport(WebSocketTransportRegistration registration) {
registration.setSendTimeLimit(15 * 1000).setSendBufferSizeLimit(512 * 1024);
}
// ...
}
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:websocket="http://www.springframework.org/schema/websocket"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/websocket
http://www.springframework.org/schema/websocket/spring-websocket-4.0.xsd">
<websocket:message-broker>
<websocket:transport send-timeout="15000" send-buffer-size="524288" />
<!-- ... -->
</websocket:message-broker>
</beans>
위의 WebSocket 전송 설정은 들어오는 STOMP messages 에 허락되는 최대 사이즈를 설정하는데 사용될 수 있다. 비록 이론적으로 WebSocket message 가 size 에 제한이 없지만, 실제 WebSocket servers 는 한계를 부여한다. 예를 들어 Tomcat 에서 8K 이고 Jetty 에서 64K 이다. 이런 이유로 stomp.js 같은 STOMP clients 는 커다란 STOMP messages 를 16K boundaries 로 분리하고 그들을 복수개의 WebSocket messages 로 전송한 후 서버가 buffer 해서 재조합하도록 한다.
Spring 의 STOMP over WebSocket 지원은 이것을 수행한다. 그래서 applications 은 STOMP messages 를 위해 WebSocket server 고유의 message sizes 를 무시한 채 maximum size 를 설정할 수 있다. WebSocket message size 가 만약 최소 16K WebSocket messages 전송을 보장하여야 한다면 자동적으로 조정되어질 것을 명심하라.
다음은 설정예시이다.
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void configureWebSocketTransport(WebSocketTransportRegistration registration) {
registration.setMessageSizeLimit(128 * 1024);
}
// ...
}
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:websocket="http://www.springframework.org/schema/websocket"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/websocket
http://www.springframework.org/schema/websocket/spring-websocket-4.0.xsd">
<websocket:message-broker>
<websocket:transport message-size="131072" />
<!-- ... -->
</websocket:message-broker>
</beans>
scaling 에 대한 중요한 point 는 복수개의 application instances 를 사용하는 것이다. 현재 simple broker 로는 불가능하다. 그러나 RabbitMQ 같은 full-featured broker 를 사용할 때, 각 application instance 는 broker 에 연결하고 하나의 application instance 로부터의 messages broadcast 는 어떤 다른 application instances 를 통해 연결된 WebSocket clients 에 그 broker 를 통해 broadcast 할 수 있다.
Spring 의 STOMP over WebSocket 지원에 2가지 주요한 접근이 있다. 첫번째는 controller 의 기능과 annotated message handling method 의 기능을 검증하는 server-side tests 를 작성하는 것이다. 두번째는 client 와 server 구동과 관련한 전체 end-to-end tests 를 작성하는 것이다.
2 가지 접근은 상호 보완적이지 않다. 그와는 반대로 전체 test 전략에 각각 위치한다. Server-side tests 는 좀더 집중되고 작성하고 유지하기가 쉽다. 반면에 End-to-end integration tests 는 좀 더 완벽하고 테스트가 더 많지만 그것들은 작성과 유지에 더 많이 관련되어 진다.
가장 단순한 server-side tests 는 controller unit tests 를 작성하는 것이다. 그러나 이것은 충분히 유용하지 않다. 왜냐하면 controller 의 많은 부분이 annotations 에 의존하기 때문이다. 순수한 unit tests 는 그것을 테스트하지 못한다.
이상적으로 테스트 하의 controllers 는 마치 SpringMVC Test framework 에서 HTTP 요청을 처리하는 controller 를 테스트하는 접근같이 runtime 에서 처럼 호출되어야 한다. 즉 구동중인 Servlet container 없이 Spring Framework 가 annotated controllers 를 호출하는 것이다. Spring MVC Test 와 같이 2가지 가능한 대안이 있다. 그것은 “context-based” 나 “standalone” setup 을 사용하는 것이다.
Spring TestContext framework 의 도움으로 실제 Spring configuration 로드하고 “clientInboundChannel” 를 테스트 field 로 주입받아서, 이를 사용하여 메시지를 전송한다. 수동적으로 controllers 를 호출하기 위해 필요한 최소한의 Spring framework infrastructure 를 설정한다. (SimpAnnotationMethodMessageHandler) 그리고 메시지를 controller 에 직접 전달한다. 이 두가지 시나리오는 tests for the stock portfolio sample application 에서 시연되고 있다.
두번째 접근은 end-to-end integration tests 를 만드는 것이다. 이를 위해 WebSocket server 를 embedded mode 로 구동할 필요가 있고 STOMP frame 을 포함하는 메시지를 전송하는 WebSocket client 로서 서버에 접속한다. stock portfolio sample application 에 대한 test 는 embedded WebSocket 서버로서 Tomcat 을 사용하고 test 목적의 간단한 STOMP client 를 사용하는 접근을 보여준다.
WebSocket 이 아직까지 모든 브라우저에서 지원되지 않거나 네트워크 프락시 제약등으로 사용할 수 없는 경우가 있다. 이에 Spring 은 fallback 옵션을 제공하는데 이는 SockJS protocol 에 기반으로 WebSocket API 를 emulate 한다.
SockJS 는 application 으로 하여금 WebSocket API 를 사용하는데 있다. 만약 WebSocket 사용이 불가한 경우에도 이를 fallback option 으로 제공하여 어떠한 코드 변화없이 WebSocket API 를 사용토록 한다.
SockJS 는 여러가지 테크닉을 이용하여 다양한 브라우저 및 브라우저 버전을 지원한다. 전송 타입은 다음의 3가지로 분류된다. WebSocket, HTTP Streaming, HTTP Long Polling. 이들 각각을 살펴보려면 여기 를 참조한다.
SockJS client 는 서버의 기본 정보를 얻기 위해서 “GET /info” 를 호출한다. 그 이후에 SockJS 는 어떤 전송 타입을 사용할지를 결정한다. WebSocket 은 최우선책이며, 이후로 HTTP Streaming > HTTP (long) polling 이 사용된다.
모든 전송 요청은 다음의 URL 구조를 갖는다.
http://host:port/myApp/myEndpoint/{server-id}/{session-id}/{transport}
WebSocket 전송은 WebSocket handshaking 을 위한 오직 하나의 HTTP 요청을 필요로 한다. 모든 메시지들은 그 이후에 사용했던 socket 을 통해 교환된다.
HTTP 전송은 좀 더 많은 요청을 필요로 한다. 예를 들어 Ajax/XHR streaming 은 server 에서 client 로의 메시지를 위해 하나의 long-running 요청이 있고 추가적인 HTTP POST 요청은 client 에서 server 로의 메시지를 위해 사용된다. Long polling 은 server 가 client 로의 응답 후에 현재의 요청을 끝내는 것을 제외하고는 XHR streaming 과 유사하다.
SockJS 는 최소한의 message framing 을 추가한다. 예를 들어 server 는 “o” (open frame) 를 초기에 전송하고, 메시지는 [“message1”,”message2”] 와 같은 JSON-encoded 배열로서 전달되며, 문자 “h” (hearbeat frame) 는 기본적으로 25초간 메시지 흐름이 없는 경우에 전송하고 “c” (close frame) 는 해당 세션을 종료한다.
이에 대한 자세한 이해는 브라우저에서 확인할 수 있다. SockJS 는 debug flag 를 제공하고, 전송타입을 고정하여 각각에 대해서 살펴볼 수 있다. 서버는 “org.springframework.web.socket” 에 TRACE 로깅을 활성화 하여 로그를 볼 수 있다.
SockJS 는 설정을 통해 쉽게 활성화 할 수 있다.
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(myHandler(), "/myHandler").withSockJS();
}
@Bean
public WebSocketHandler myHandler() {
return new MyHandler();
}
}
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:websocket="http://www.springframework.org/schema/websocket"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/websocket
http://www.springframework.org/schema/websocket/spring-websocket-4.0.xsd">
<websocket:handlers>
<websocket:mapping path="/myHandler" handler="myHandler"/>
<websocket:sockjs/>
</websocket:handlers>
<bean id="myHandler" class="org.springframework.samples.MyHandler"/>
</beans>
위의 설정은 Spring MVC 에서 사용하기 위한 것으로 DispatcherServlet 설정에 포함되어져야 한다. (application context 를 계층적으로 가져갈 경우) 그러나 Spring WebSocket 이 SpringMVC 이외에도 사용할 수 있도록 제공되듯, SockJS 도 그러하다. 이는 SockJsHttpRequestHandler 를 통해서 제공된다.
browser (클라이언트) 측에서는 W3C WebSocket API 를 emulate 하는 sockjs-client 를 사용할 수 있다. 이를 통해 서버와 통신하여 최적의 전송타입을 선택한다. 이 외에도 포함할 전송타입을 지정하는 몇가지 설정을 제공한다.
IE 8/9 는 대중적으로 사용되고 있다. 이것은 SockJS 가 필요한 핵심적인 이유이다.
SockJS 는 Ajax/XHR Streaming 을 Microsoft 의 XDomainRequest 를 통해 지원하고 있다. 이것은 서로 다른 도메인 간에도 동작하지만 cookie 전송을 지원하지 않는다. Cookie 는 Java Application 에서 매우 필수적이다. 그러나 SockJS 클라이언트는 Java 만을 위한 것이 아닌 많은 서버 타입을 위해 사용되도록 고안되었기 때문에 Cookie 를 중요하게 다룰지 여부를 알려주어야 한다. SockJS 클라이언트는 Ajax/XHR Streaming 을 선택하거나 그렇지 않다면 iframe 기반의 technique 을 사용한다.
SockJS 클라이언트로 부터의 최초 요청인 ‘/info’ 는 클라이언트의 전송 타입 선택을 위한 정보를 위한 요청이다. 상세한 내용 중 하나는 서버 application 이 cookie 에 의존 (authentication 목적이나 session clustering with stick mode 등) 하는지 여부이다. Spring 의 SockJS 지원은 sessionCookieNeeded 라는 속성을 포함한다. 이것은 Java application 이 JSESSIONID cookie 에 의존하기 때문에 기본적으로 활성화 된다. 만약 이 기능을 OFF 한다면 비로서 SockJS 클라이언트는 xdr-streaming 을 IE 8/9 에서 사용토록 선택되어 진다.
iframe 기반의 전송을 사용한다면 browser 가 HTTP 응답헤더인 X-Frame-Option 이 DENY, SAMEORIGIN, ALLOW-FROM <origin> 로 설정됨에 따라 iframe 사용을 블락시킬수 있음을 염두에 두어야 한다. 이러한 헤더는 clickjacking 이라 알려진 공격을 방어하기 위한 목적으로 주로 사용된다.
Spring Security 3.2+ 에서 X-Frame-Options 헤더를 모든 응답에 설정하도록 지원하고 있다. 즉 Spring Security Java Config 에서 이 값을 DENY 로 기본설정한다. Spring Security XML 설정에서는 이 헤더를 설정하지 않지만 차후에 기본값으로 설정될 여지가 있다.
Spring Security 의 Section 7.1. Default Security Headers 에서 X-Frame-Options 헤더 설정에 대한 상세 문서를 살펴볼 수 있다. 이에 대한 추가적인 배경을 보고자 한다면 SEC-2501 을 살펴본다.
X-Frame-Option 응답헤더를 추가한다면 (Spring Security 를 사용한다면 그렇게 된다.) 헤더 값을 SAMEORIGIN 이나 ALLOW-FROM <origin> 으로 설정할 필요가 있다. 이와 더불어 Spring SockJS 지원은 SockJS 클라이언트의 location 을 알아야 할 필요가 있다. 왜냐하면 client 가 iframe 상에서 로드될 것이기 때문이다. 기본적으로 iframe 은 SockJS 클라이언트 (sockJS.js) 를 CDN location 으로부터 다운되도록 설정되어 있다. 이를 same origin 으로 설정하는 것이 필요하다.
Java Config 에서 아래와 같이 설정할 수 있다. XML 에서는 <websocket:sockjs> 에서 설정한다.
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/portfolio").withSockJS()
.setClientLibraryUrl("http://localhost:8080/myapp/js/sockjs-client.js");
}
// ...
}
초기 개발중에는 SockJS client devel mode 를 활성화 하라. 이는 SockJS request (iframe 같은) 를 브라우저가 캐시하는 것을 방지한다. 이에 대한 방법은 SockJS client page 에서 확인할 수 있다.
SockJS 프로토콜은 server 가 hearbeat message 를 전송하도록 하여 proxies 가 connection 을 hung 으로 인식하지 못하도록 할 필요가 있다. Spring SockJS 설정은 heartbeatTime 속성을 가지는데 빈도를 조정하는데 사용된다. 기본값은 해당 커넥션에 어떤 메시지도 없는 25초를 사용한다. 이 25초는 IETF 권고안 을 따르고 있다.
STOMP over WebSocket/SockJS 를 사용할 때 만약 STOMP 클라이언트와 서버가 heartbeat 교환에 합의한다면 SockJS heartbeat 은 비활성화 된다.
Spring SockJS 지원은 heartbeat 작업을 스케줄링하기 위해 TaskScheduler 를 설정하도록 하고 있다. TaskScheduler 는 이용가능한 processor 의 수에 따른 기본값으로 설정된 thread pool 로부터 가져오는데 이는 특정한 요구에 적합한 값을 설정하는 것을 고려해야 한다.
HTTP streaming 과 HTTP long polling SockJS 전송은 커넥션을 보통의 경우보다 오래 open 되도록 한다. 이것에 대한 개요는 다음 에서 확인한다.
Servlet Container 에서 이것은 Servlet 3 async 지원을 통해 수행되는데 이것은 요청을 처리중인 Servlet 스레드를 빠져나오고 다른 Servlet 스레드에서 응답을 기록하도록 한다.
Servlet API 가 가지는 특정 이슈가 있는데 그것은 클라이언트의 갑작스러운 사라짐에 어떠한 알림도 제공하지 않는 것이다. ( SERVLET_SPEC-44 참고) 그러나 Servlet container 는 응답을 write 하기 위한 이어지는 시도에 예외를 발생한다. Spring SockJS 지원은 기본 25초 간격의 heartbeat message 를 전송하므로 이를 통해 보통 client disconnect 는 주기 안에 발견되어 진다. (주기를 짧게 하면 더 빨리 알게됨)
결론적으로, Network IO failure 은 client disconnect 에 대해서 빈번하게 발생할 여지가 있다. 이것은 로그를 stack trace 로 채우게 될 수도 있다. Spring 은 이러한 client disconnect 를 식별하기위해 최선의 노력을 하고 이에 따른 최소한의 메시지를 기록하려고 노력한다. (이 로그는 AbstractSockJsSession 에 정의된 DISCONNECTED_CLIENT_LOG_CATEGORY 로그 카테고리를 사용한다. 만약 stack trace 를 보고자 한다면 해당 로그 카테고리를 TRACE 로 설정하라)
SockJS protocol 은 XHR streaming 과 polling 전송에 cross-domain 지원을 위한 CORS 헤더를 사용한다. 그래서 CORS 헤더가 응답에서 발견되지 않는다면 CORS 헤더들이 자동으로 추가된다. 만약 Servlet Filter 등을 통해서 CORS 헤더가 이미 설정된다면 Spring SockJsService 는 이를 스킵한다.
다음은 SockJS 에 의해 기대되는 header 와 값들이다.
정확한 구현을 보고자 한다면 AbstractSockJsService.addCorsHeaders() 와 TransportType enum 을 소스에서 살펴보라.
대안으로서 CORS 설정은 SockJS endpoint prefix 를 제외 URL 목록으로 고려한다면, Spring SockJsService 가 그것을 처리토록 한다.
부트스트랩(Bootstrap)은 웹디자인을 쉽게 하기 위해 트위터에서 오픈 소스로 공개한 프런트 엔드 프레임워크로, 유연한 HTML, CSS, JavaScript 템플릿과 UI컴포넌트, 인터렉션을 제공하여 손 쉽게 웹 사이트를 구축할 수 있는 시작점이 된다.
부트스트랩의 장점은 크게 다음과 같다.
이러한 장점들로 인해 표준프레임워크에서는 실행환경 UI로 bootstrap을 선정하였다.
본 가이드에서는 부트스트랩의 기본적인 소개와 몇 가지의 예제를 제공한다.
자세한 내용은 부트스트랩 사이트의 가이드나 w3schools의 튜토리얼을 참조하도록 한다.
부트스트랩을 사용하기 위해서는 부트스트랩 관련 CSS와 JavaScript를 추가해 주어야 한다. 추가하는 방법은 두가지로 부트스트랩을 다운로드 하여 사용하거나, 다운로드 하지 않을 경우 CDN 링크를 추가하여 사용 한다.
※ 주의사항 : 부트스트랩을 사용하려면 jQuery가 필요하므로 반드시 별도로 추가하여 주어야 한다.
<!-- 최신 컴파일 및 최소화된 최신 CSS -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css"
integrity="sha384-1q8mTJOASx8j1Au+a5WDVnPi2lkFfwwEAa8hDDdjZlpLegxhjVME1fgjWPGmkzs7" crossorigin="anonymous">
<!-- 옵션 테마 -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap-theme.min.css"
integrity="sha384-fLW2N01lMqjakBkx3l/M9EahuwpSfeNvV63J5ezn3uZzapT0u7EYsXMjQV+0En5r" crossorigin="anonymous">
<!-- jQuery library 부트스트랩을 사용하려면 jQuery가 필요함-->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.2/jquery.min.js"></script>
<!-- 최신 컴파일 및 최소화된 자바스크립트 -->
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js"
integrity="sha384-0mSbJDEHialfmuBBQP6A4Qrprq5OVfW37PRR3j5ELqxss1yVqOtnepnHVP9aJ7xS" crossorigin="anonymous"></script>
프리 컴파일 된 부트스트랩을 다운로드하고, 압축을 해제하면 다음과 같은 구조를 볼 수 있다.
bootstrap/
├── css/
│ ├── bootstrap.css
│ ├── bootstrap.css.map
│ ├── bootstrap.min.css
│ ├── bootstrap-theme.css
│ ├── bootstrap-theme.css.map
│ └── bootstrap-theme.min.css
├── js/
│ ├── bootstrap.js
│ └── bootstrap.min.js
└── fonts/
├── glyphicons-halflings-regular.eot
├── glyphicons-halflings-regular.svg
├── glyphicons-halflings-regular.ttf
├── glyphicons-halflings-regular.woff
└── glyphicons-halflings-regular.woff2
위의 구조는 어느 웹프로젝트에도 쉽게 적용하기 위한 가장 기본적인 형태다. 컴파일 된 CSS와 JavaScript(bootstrap.)를 제공하고, 컴파일 되고 최소화된 CSS와 JavaScript(bootstrap.min.) 도 제공한다.
그리고 Glyphicons 의 폰트 파일들과 부가적인 부트스트랩 테마 파일들도 같이 존재한다.
부트스트랩은 HTML5의 HTML요소와 CSS속성을 요구하기 때문에 HTML5 doctype을 사용해야 한다. 항상 페이지의 시작부분에 HTML5 doctype과 <head>내에 lang 속성과 character set을 추가해 주어야 한다.
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="utf-8">
</head>
</html>
부트스트랩2 에서는 모바일 친화적인 스타일을 프레임워크의 core로 추가했는데, 부트스트랩3 부터는 시작부터 모바일 친화적으로 설계 되었다.
다음은 페이지의 렌더링과 확대/축소를 사용하기 위한 것으로, <head> 내에 viewport 메타 태그를 추가한다.
<meta name="viewport" content="width=device-width, initial-scale=1">
부트스트랩은 사이트 전체의 콘텐츠를 감싸는 컨테이너 요소가 필요하다.
다음의 두 예제는 컨테이너에 대한 예제이다.
※ jsfiddle.net을 이용하여 아래의 예제코드를 실행 결과를 링크로 제공.
<!DOCTYPE html>
<html lang="ko">
<head>
<title>Bootstrap Example</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css"
integrity="sha384-1q8mTJOASx8j1Au+a5WDVnPi2lkFfwwEAa8hDDdjZlpLegxhjVME1fgjWPGmkzs7" crossorigin="anonymous">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.2/jquery.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js"
integrity="sha384-0mSbJDEHialfmuBBQP6A4Qrprq5OVfW37PRR3j5ELqxss1yVqOtnepnHVP9aJ7xS" crossorigin="anonymous"></script>
</head>
<body>
<div class="container">
<h1>My First Bootstrap Page</h1>
<p>This is some text.</p>
</div>
</body>
</html>
결과 보기 : https://jsfiddle.net/ymxvbzo7/?utm_source=website&utm_medium=embed&utm_campaign=ymxvbzo7
<!DOCTYPE html>
<html lang="ko">
<head>
<title>Bootstrap Example</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css"
integrity="sha384-1q8mTJOASx8j1Au+a5WDVnPi2lkFfwwEAa8hDDdjZlpLegxhjVME1fgjWPGmkzs7" crossorigin="anonymous">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.2/jquery.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js"
integrity="sha384-0mSbJDEHialfmuBBQP6A4Qrprq5OVfW37PRR3j5ELqxss1yVqOtnepnHVP9aJ7xS" crossorigin="anonymous"></script>
</head>
<body>
<div class="container-fluid">
<h1>My First Bootstrap Page</h1>
<p>This is some text.</p>
</div>
</body>
</html>
결과 보기 : https://jsfiddle.net/y1hL9vqd/1/?utm_source=website&utm_medium=embed&utm_campaign=y1hL9vqd


위의 그림처럼 두 예제의 결과보기의 화면의 폭을 좌우로 늘려보면, 두 예제의 차이점을 알 수 있다.
첫번째 예제는 고정 폭 컨테이너(<div class="container">)를 사용하였고, 두번째 예제는 최대 폭 컨테이너(<div class="container-fluid">)를 사용한 예제이다.
그 밖에 세부 내용은 링크를 참조하도록 한다.
부트스트랩은 반응형, 페이지 레이아웃을 위해 자체적인 그리드 레이아웃 시스템을 제공한다.
부트스트랩의 그리드 시스템은 12열의 그리드(12-column grid)로 구성되어 있으며, device나 viewport의 크기에 따라 자동으로 열이 적절한 크기로 배열되게 한다.
기타사항은 링크 내용을 참조하도록 한다.
| 매우 작은 기기 모바일 폰( < 768px) | 작은 기기 태블릿 (≥768px) | 중간 기기 데스크탑 (≥992px) | 큰 기기 데스크탑 (≥1200px) | |
|---|---|---|---|---|
| 그리드 적용 | 항상 | 분기점보다 크면 적용 | ||
| 컨테이너 너비 | 없음 (auto) | 750px | 970px | 1170px |
| 클래스 접두사 | .col-xs- | .col-sm- | .col-md- | .col-lg- |
| 컬럼 수 | 12 | |||
| 컬럼 너비 | Auto | ~62px | ~81px | ~97px |
| 사이 너비 | 30px (컬럼의 양쪽에 15px 씩) |
세부내용은 링크의 표를 참조한다.
<div class="row">
<div class="col-sm-4">.col-sm-4</div>
<div class="col-sm-4">.col-sm-4</div>
<div class="col-sm-4">.col-sm-4</div>
</div>
<div class="row">)<div class="col-sm-4">.col-sm-4</div>)<!DOCTYPE html>
<html lang="ko">
<head>
<title>Bootstrap Example</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css"
integrity="sha384-1q8mTJOASx8j1Au+a5WDVnPi2lkFfwwEAa8hDDdjZlpLegxhjVME1fgjWPGmkzs7" crossorigin="anonymous">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.2/jquery.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js"
integrity="sha384-0mSbJDEHialfmuBBQP6A4Qrprq5OVfW37PRR3j5ELqxss1yVqOtnepnHVP9aJ7xS" crossorigin="anonymous"></script>
</head>
<body>
<div class="container">
<h1>Grid</h1>
<p>This example demonstrates a 50%/50% split on small, medium and large devices. On extra small devices, it will stack (100% width).</p>
<p>Resize the browser window to see the effect.</p>
<div class="row">
<div class="col-sm-6" style="background-color:yellow;">
Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.<br>
Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
</div>
<div class="col-sm-6" style="background-color:pink;">
Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto.
</div>
</div>
</div>
</body>
</html>

결과 보기 : https://jsfiddle.net/eh5kmtt9/1/?utm_source=website&utm_medium=embed&utm_campaign=eh5kmtt9
결과창의 좌우폭을 확대 및 축소를 해보면, 화면 사이즈에 맞춰서 열(column)이 쌓이거나(stack) 수평이 되게 늘어나는 것을 확인 할 수 있다.이와 같이 손 쉽게 반응형을 지원하고, 페이지 레이아웃을 구성할 수 있다.
그 밖의 예제들은 부트스트랩 사이트 그리드 부분에서 내용을 확인하도록 한다.
부트스트랩은 깔끔하고 잘 정돈된 느낌의 다양한 스타일의 CSS를 제공한다. 자세한 내용은 링크의 내용을 확인하도록 한다.
다음은 부트스트랩에서 제공하는 클래스를 적용한 테이블 예제이다.
<!DOCTYPE html>
<html lang="ko">
<head>
<title>Bootstrap Example</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css"
integrity="sha384-1q8mTJOASx8j1Au+a5WDVnPi2lkFfwwEAa8hDDdjZlpLegxhjVME1fgjWPGmkzs7" crossorigin="anonymous">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.2/jquery.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js"
integrity="sha384-0mSbJDEHialfmuBBQP6A4Qrprq5OVfW37PRR3j5ELqxss1yVqOtnepnHVP9aJ7xS" crossorigin="anonymous"></script>
</head>
<body>
<div class="container">
<h2>Contextual Classes</h2>
<p>Contextual classes can be used to color table rows or table cells. The classes that can be used are: .active, .success, .info, .warning, and .danger.</p>
<table class="table">
<thead>
<tr>
<th>Firstname</th>
<th>Lastname</th>
<th>Email</th>
</tr>
</thead>
<tbody>
<tr class="success">
<td>John</td>
<td>Doe</td>
<td>example1@example.com</td>
</tr>
<tr class="danger">
<td>Mary</td>
<td>Moe</td>
<td>example2@example.com</td>
</tr>
<tr class="info">
<td>July</td>
<td>Dooley</td>
<td>example3@example.com</td>
</tr>
</tbody>
</table>
</div>
</body>
</html>
결과보기 : https://jsfiddle.net/r60oymr2/3/?utm_source=website&utm_medium=embed&utm_campaign=r60oymr2

부트스트랩에서 기본적으로 제공하는 테이블 디자인 중 하나이다.
다음은 부트스트랩에서 제공하는 폼의 예제이다.
<!DOCTYPE html>
<html lang="ko">
<head>
<title>Bootstrap Example</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css"
integrity="sha384-1q8mTJOASx8j1Au+a5WDVnPi2lkFfwwEAa8hDDdjZlpLegxhjVME1fgjWPGmkzs7" crossorigin="anonymous">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.2/jquery.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js"
integrity="sha384-0mSbJDEHialfmuBBQP6A4Qrprq5OVfW37PRR3j5ELqxss1yVqOtnepnHVP9aJ7xS" crossorigin="anonymous"></script>
</head>
<body>
<div class="container">
<h2>Inline form</h2>
<p>Make the viewport larger than 768px wide to see that all of the form elements are inline, left aligned, and the labels are alongside.</p>
<form class="form-inline" role="form">
<div class="form-group">
<label for="email">Email:</label>
<input type="email" class="form-control" id="email" placeholder="Enter email">
</div>
<div class="form-group">
<label for="pwd">Password:</label>
<input type="password" class="form-control" id="pwd" placeholder="Enter password">
</div>
<div class="checkbox">
<label><input type="checkbox"> Remember me</label>
</div>
<button type="submit" class="btn btn-default">Submit</button>
</form>
</div>
</body>
</html>
결과보기 : https://jsfiddle.net/4006oqs0/1/?utm_source=website&utm_medium=embed&utm_campaign=4006oqs0

위의 예제는 인라인 폼(inline form)이 적용 된 예제로, 화면의 폭에 따라 입력창의 위치가 변화한다.
다음은 부트스트랩에서 제공하는 버튼 디자인이다.
<!DOCTYPE html>
<html lang="ko">
<head>
<title>Bootstrap Example</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css"
integrity="sha384-1q8mTJOASx8j1Au+a5WDVnPi2lkFfwwEAa8hDDdjZlpLegxhjVME1fgjWPGmkzs7" crossorigin="anonymous">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.2/jquery.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js"
integrity="sha384-0mSbJDEHialfmuBBQP6A4Qrprq5OVfW37PRR3j5ELqxss1yVqOtnepnHVP9aJ7xS" crossorigin="anonymous"></script>
</head>
<body>
<div class="container">
<h2>Button</h2>
<!-- Standard button -->
<button type="button" class="btn btn-default">Default</button>
<!-- Provides extra visual weight and identifies the primary action in a set of buttons -->
<button type="button" class="btn btn-primary">Primary</button>
<!-- Indicates a successful or positive action -->
<button type="button" class="btn btn-success">Success</button>
<!-- Contextual button for informational alert messages -->
<button type="button" class="btn btn-info">Info</button>
<!-- Indicates caution should be taken with this action -->
<button type="button" class="btn btn-warning">Warning</button>
<!-- Indicates a dangerous or potentially negative action -->
<button type="button" class="btn btn-danger">Danger</button>
<!-- Deemphasize a button by making it look like a link while maintaining button behavior -->
<button type="button" class="btn btn-link">Link</button>
</div>
</body>
</html>
결과보기 : https://jsfiddle.net/ysohpdqh/5/?utm_source=website&utm_medium=embed&utm_campaign=ysohpdqh

부트스트랩에서는 iconography, dropdown, input group, navigation, alerts 등의 재사용 가능한 컴포넌트를 제공하고 있다. 본 가이드에서는 몇 가지 컴포넌트만 소개한다.
그 밖의 다른 컴포넌트 들은 링크를 통해 확인 하도록 한다.
250개 이상의 기호가 Glyphicon Halflings 세트로 폰트 포맷에 포함되어 있다.
![]()
사용할 부분에 다음 구문을 삽입한다.
<span class="glyphicon glyphicon-search" aria-hidden="true"></span>
위의 구문에서 원하는 Glyphicon의 class를 교체하여 사용한다.
다음은 Glyphicon을 사용한 예제이다.
<!DOCTYPE html>
<html lang="ko">
<head>
<title>Bootstrap Example</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css"
integrity="sha384-1q8mTJOASx8j1Au+a5WDVnPi2lkFfwwEAa8hDDdjZlpLegxhjVME1fgjWPGmkzs7" crossorigin="anonymous">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.2/jquery.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js"
integrity="sha384-0mSbJDEHialfmuBBQP6A4Qrprq5OVfW37PRR3j5ELqxss1yVqOtnepnHVP9aJ7xS" crossorigin="anonymous"></script>
</head>
<body>
<div class="container">
<h2>Glyphicon Examples</h2>
<p>Envelope icon: <span class="glyphicon glyphicon-envelope"></span></p>
<p>Envelope icon as a link:
<a href="#"><span class="glyphicon glyphicon-envelope"></span></a>
</p>
<p>Search icon: <span class="glyphicon glyphicon-search"></span></p>
<p>Search icon on a button:
<button type="button" class="btn btn-default">
<span class="glyphicon glyphicon-search"></span> Search
</button>
</p>
<p>Search icon on a styled button:
<button type="button" class="btn btn-info">
<span class="glyphicon glyphicon-search"></span> Search
</button>
</p>
<p>Print icon: <span class="glyphicon glyphicon-print"></span></p>
<p>Print icon on a styled link button:
<a href="#" class="btn btn-success btn-lg">
<span class="glyphicon glyphicon-print"></span> Print
</a>
</p>
</div>
</body>
</html>
결과보기 : https://jsfiddle.net/t1tresaj/1/?utm_source=website&utm_medium=embed&utm_campaign=t1tresaj
![]()
페이지네이션 컴포넌트로 사이트나 앱을 위한 페이지네이션 링크를 제공한다.
페이지네이션의 경우 순서가 없는 목록(unordered list, <ul> )에 .pagination class 클래스를 추가한다.
다음은 페이지네이션을 사용한 예제이다.
<!DOCTYPE html>
<html lang="ko">
<head>
<title>Bootstrap Example</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css"
integrity="sha384-1q8mTJOASx8j1Au+a5WDVnPi2lkFfwwEAa8hDDdjZlpLegxhjVME1fgjWPGmkzs7" crossorigin="anonymous">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.2/jquery.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js"
integrity="sha384-0mSbJDEHialfmuBBQP6A4Qrprq5OVfW37PRR3j5ELqxss1yVqOtnepnHVP9aJ7xS" crossorigin="anonymous"></script>
</head>
<body>
<div class="container">
<h2>Pagination - Active State</h2>
<p>Add class .active to let the user know which page he/she is on:</p>
<ul class="pagination">
<li><a href="#">1</a></li>
<li class="active"><a href="#">2</a></li>
<li><a href="#">3</a></li>
<li><a href="#">4</a></li>
<li><a href="#">5</a></li>
</ul>
</div>
</body>
</html>
결과보기 : https://jsfiddle.net/L860z1cy/1/?utm_source=website&utm_medium=embed&utm_campaign=L860z1cy

위의 예제에서 리스트 아이템(list item, <li>)에 .active class를 추가하면, 해당 링크는 활성 상태가 된다.
클릭할 수 없는 링크일 경우 .disable을 사용하면 비활성 상태로 된다.
부트스트랩 내에 존재하는 UI 컴포넌트에 동적인 인터랙션이 필요한 컴포넌트는 12개가 넘는 jQuery plugin을 통하여 컨트롤 할 수 있도록 되어있다.
단 컴포넌트를 주의할 점은 모든 플러그인은 jQuery에 의존하기 때문에 jQuery는 반드시 플러그인 파일 전에 포함되어야 한다.
본 가이드에서는 모달(modal)에 대한 가이드만 제공한다.
모달 플러그인은 현재 페이지의 상단에 표시되는 대화 상자 / 팝업 창이다.
다음은 기본적인 modal을 만드는 방법을 보여주는 예제이다.
<!DOCTYPE html>
<html lang="ko">
<head>
<title>Bootstrap Example</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css"
integrity="sha384-1q8mTJOASx8j1Au+a5WDVnPi2lkFfwwEAa8hDDdjZlpLegxhjVME1fgjWPGmkzs7" crossorigin="anonymous">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.2/jquery.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js"
integrity="sha384-0mSbJDEHialfmuBBQP6A4Qrprq5OVfW37PRR3j5ELqxss1yVqOtnepnHVP9aJ7xS" crossorigin="anonymous"></script>
</head>
<body>
<div class="container">
<h2>Modal Example</h2>
<!-- Trigger the modal with a button -->
<button type="button" class="btn btn-info btn-lg" data-toggle="modal" data-target="#myModal">Open Modal</button>
<!-- Modal -->
<div class="modal fade" id="myModal" role="dialog">
<div class="modal-dialog">
<!-- Modal content-->
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal">×</button>
<h4 class="modal-title">Modal Header</h4>
</div>
<div class="modal-body">
<p>Some text in the modal.</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
</div>
</body>
</html>
결과보기 : https://jsfiddle.net/eqfmxd3e/1/?utm_source=website&utm_medium=embed&utm_campaign=eqfmxd3e

위의 예제의 구조를 보면 크게 Triger, Modal, Modal content로 구성된 것을 확인할 수 있다.
<div>에는 모달(myModal)을 트리거 하기 위해 사용되는 data-target의 속성 값과 동일한 ID를 가져야 한다.<div>를 식별하고, 그것을 focus로 가져온다.<div>는 모달의 스타일(border, background-color 등…)을 지정한다. 여기에 모달의 header, body, footer를 추가한다.<button>에는 data-dismiss=modal 속성이 정의되어 있는데 이는 클릭하면 모달을 닫는 기능을 수행한다.전자정부에서 효율적인 스마트 전자정부 기반시스템의 구축•운영을 통해 전자정부의 서비스 품질 UX 레이어는 UI/UX Controller Component, JavaScript Module App Framework, HTML5, CSS3 서비스를 제공한다. 오픈소스는 JQuery Mobile을 채택하였으며 jQuery Mobile은 html5, CSS3, javascript를 제공한다. 오픈 소스를 Customizing 하여 UI레이어의 기능을 사용 하며 내용은 아래와 같다 UI/UX Controller Component 모바일 웹 사용자 환경(UX/UI)에 대한 유연한 대응을 위해 Touch Optimized 된 필수 UI 컨트롤러 컴포넌트를 제공한다. HTML5는 모바일 웹 페이지 구성 시 사용 할 수 있는 마크업 언어로서 모바일 특화 태그 밑 디바이스 API를 제공한다. CSS3는 모바일 기기 및 브라우저에 따라 적합한 컴포넌트가 보여지는 기능을 제공한다. 또한 JavaScript Module App Framework UX/UI controller component의 효율성을 보장하는 javascript 밑 Json 구조를 제공한다.
UX 처리 레이어는 모바일 환경을 화면을 담당하는 레이어로 화면 구성을 위한 Button, Panel, Internal/Externel Link, Process Dialog/Bar, Menu, Date/Time Picker, Check, Radio, Label/Text, TABS, Form, Grid, List View ICon, Selector, Collapsible Block를 제공한다.모바일 화면에 특화된 16개의 컴포넌트를 제공한다
jQuery Mobile 은 HTML5의 doctype 으로 선언하여야 하며, jQueryMobile에서 사용하는 CSS, JS(jQuery, jQueryMobile)를 Import 함으로서 사용 할 수 있다.
jQuery Mobile은 jQuery Core를 사용하고 있다.
Page의 Header 선언 부분에 모바일 실행환경을 import한다.
<!DOCTYPE html>
<html>
<head>
<title>eGovFrame</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<link rel="stylesheet" href="/css/egovframework/mbl/cmm/jquery.mobile-1.3.2.css"/>
<link rel="stylesheet" href="/css/egovframework/mbl/cmm/EgovMobile-1.3.2.css" />
<script src="/js/egovframework/mbl/cmm/jquery-1.9.1.min.js"></script>
<script src="/js/egovframework/mbl/cmm/jquery.mobile-1.3.2.min.js"></script>
<script src="/js/egovframework/mbl/cmm/EgovMobile-1.3.2.js"></script>
</head>
<body>
...
</body>
</html>
jQuery Mobile 의 Page 구조는 div를 사용하여 표현하며 html5의 ‘data-*’ 속성을 이용하여 구조를 구분한다.
<div data-role=“page”>
<div data-role=“header”>
</div>
<div data-role=“content”>
</div>
<div data-role=“footer”>
</div>
</div>
jQuery Mobile 은 하나의 페이지를 <div data-role=“page”> 단위로 관리 하며 한 HTML 내에 여러 <div data-role=“page”> 가 있을 경우 제일 상단의 div page를 첫 화면으로 인식한다. 이들 내부 페이지 간 이동은 링크 속성에 #pageName을 사용해서 가능 하다. jQuery Mobile 은 외부 페이지 이동시 anchor 태그의 링크를 가로채서 Ajax 로 해당 URL 호출 후 호출 된 Page 의 <div data-role=“page”> 영역만 가져와서 호출 한 HTML 페이지의 DOM 에 해당 내용을 추가 한다.
| 컴포넌트 | 제공기능 |
|---|---|
| Button | 설명: 명령 수행, 옵션 선택, 다른 대화 상자 열기 등에 사용하는 컴포넌트 제공 |
| 형태: 둥근 형(radius: 1em), 사각형(radius: 0em) | |
| 배치: vertical group, horizontal group | |
| 색상: 검정, 파랑, 회색, 흰색, 노랑, 빨강, 초록 | |
| 높이: normal(39px), small (28px) | |
| 넓이: 화면에 맞게 , 텍스트에 맞게 | |
| Panel | 설명: Header/footer 와 함께 페이지를 구성하는 요소 |
| 무늬: 격자 형태 무늬 제공 | |
| 색상: 검정, 회색, 연한회색, 흰색, 노랑, 빨강, 초록 | |
| Internal / External Link | 설명: 표준 링크 기능을 제공하며 기본적으로 Ajax 를 사용한 링크 방식 제공 |
| 링크: 페이지 내부링크, 도메인 내부 링크, 외부 링크, 이메일 링크, 폰 링크, 에러 페이지 링크 | |
| Label / Text | 설명: 색상, 배치, 크기, 폰트를 지정 할 수 있는 가이드 제공 |
| 색상: 초록, 빨강, 파랑 | |
| 배치: 왼쪽, 중간, 오른쪽 | |
| 크기: 15px, 25px, 30px | |
| 폰트: helvetica, verdana, tahoma | |
| Tabs | 설명: Header와 footer 에 사용되며 탭 버튼으로 문서간 이동 기능 제공 |
| 형태: round tab(radius: 0.250em), normal tab(radius: 0em) | |
| 배치: 1, 1/2, 1/3, 1/4, 1/5, 1/2 다중행 tab | |
| 색상: 검정, 파랑, 회색, 흰색, 노랑, 빨강, 초록 | |
| Form | 설명: HTML Form 요소를 모바일 환경에 최적화하여 제공 |
| 요소: Text inputs, Search inputs, Sliders, Switches, Radio buttons, Check boxes, Selectors | |
| 색상: 검정, 파랑, 회색, 흰색, 노랑, 빨강, 초록 | |
| Menu | 설명: Dialog, Grid, List, Collapsible 컴포넌트를 사용하여 메뉴 구성 기능 제공 |
| 효과: slide, slideup, slidedown, pop, fade, flip, turn, flow, slidefade | |
| 형태: Dialog, Grid, List, Collapsible | |
| 색상: 검정, 파랑, 회색, 흰색, 노랑, 빨강, 초록 | |
| Processing Dialog / Bar | 설명: 페이지 전환 간 진행 상태를 확인 할 수 있는 Progress Dialog/Bar 제공 |
| 형태: Processing Dialog, Processing Bar | |
| 색상: 검정, 파랑, 회색, 흰색, 노랑, 빨강, 초록 | |
| Dialog | 설명: 사용자와 상호작용할 수 있는 Dialog 기능 제공 |
| 형태: Dialog, Action Sheet, Overlay, Alert, Prompt, Confirm | |
| 색상: 검정, 파랑, 회색, 흰색, 노랑, 빨강, 초록 | |
| Grid View | 설명: Grid 형태로 컨텐츠를 배치할 수 있는 컴포넌트 제공 |
| 배치: 1/2, 1/3, 1/4, 1/5, 가변 Grid View | |
| Table / List View | 설명: Table/List 형태로 컨텐츠를 배치할 수 있는 컴포넌트 제공 |
| 형태: Read-only list, Link list | |
| 기능: Nested List, Numbered List, Split Button, List Divider, Count Bubble, Thumbnail, List icon, Content Formatting, Search Filter Bar, Change Mode List, Checked List | |
| 색상: 검정, 파랑, 회색, 흰색, 노랑, 빨강, 초록 | |
| Date / Time Picker | 설명: 날짜와 시간을 선택할 수 있는 Picker 제공 |
| 형태: Android Date Picker, Popup Calendar, Android Time Picker, Flip Picker(Date, Time) | |
| Check/ Radio | 설명: Check/Radio 형태로 항목을 선택할 수 있는 기능 제공 |
| 배치: vertical group, horizontal group | |
| 색상: 검정, 파랑, 회색, 흰색, 노랑, 빨강, 초록 | |
| Icon | 설명: 모바일 어플리케이션에 가장 많이 사용되는 아이콘 제공 |
| 형태: arrow-l, arrow-r, arrow-u, arrow-d, delete, plus, minus, check, gear, refresh, forward, back, grid, star, alert, info, search, home, phone, mail, gps, audio, camera, file, mic, explorer | |
| 색상: 검정, 파랑, 회색, 흰색, 노랑, 빨강, 초록 | |
| Selector / Switch | 설명: Selector/Switch 형태로 항목을 선택할 수 있는 기능 제공 |
| 효과: pop-up, list | |
| 기능: 다중선택, 단일 선택 | |
| 모양: 둥근 형(radius: 1em), 사각형(radius: 0em) | |
| 넓이: 화면에 맞게, 텍스트에 맞게 | |
| 효과: Shadow 적용, Shadow 제거 | |
| 형태: 비그룹, 그룹 | |
| 색상: 검정, 파랑, 회색, 흰색, 노랑, 빨강, 초록 | |
| Collapsible Block | 설명: 콘텐츠 영역을 접었다 펼 수 있는 컨트롤 기능 제공 |
| 형태: normal, Group, Nested | |
| 색상: 검정, 파랑, 회색, 흰색, 노랑, 빨강, 초록 |
테스트 디바이스

테스트 브라우저

모바일 표준프레임워크 사용자경험(UX)지원 브라우저 내용

전자정부에서 효율적인 스마트 전자정부 기반 시스템의 구축•운영을 위해 전자정부의 서비스 품질 UX 레이어로 UI/UX Controller Component, HTML5, CSS3, JavaScript Module App Framework 서비스를 제공한다. 오픈소스는 JQuery Mobile을 채택하였으며 jQuery Mobile은 html5, CSS3, javascript를 제공한다. 이를 Customizing 하여 UI 레이어의 기능을 사용하며 내용은 아래와 같다.
전자정부 모바일 표준프레임워크 실행환경은 기존 전자정부 표준프레임워크의 디렉터리 구조 및 표준을 준수하고 있으며, 모바일 웹 개발에 편의를 제공하기 위해 하위 디렉터리 구조를 다음과 같이 구성하고 있다.
프로젝트의 하위 폴더인 ‘src’에 실행환경을 지원하는 라이브러리 및 JSP 파일이 존재한다. 라이브러리는 CSS, JavaScript 및 이미지 파일로 구성되어 있다.

전자정부 모바일 표준 프레임워크는 CSS 및 JavaScript를 이용하여 실행환경을 제공하며 CSS, javascript, image는 각각 유기적으로 연결되어 있다.

HTML5

모바일 페이지 이동은 기본적으로 Ajax를 이용하여 처리된다. 이는 모바일에 최적화된 화면 전환 효과를 주기 위함으로 옵션 설정을 통해 변경 가능하다.
페이지 내부 이동
하나의 HTML 파일 안에 여러 page가 선언되어 있는 경우에 사용할 수 있는 방법으로 모바일 page 구성의 기본 방식이다.
페이지 내부 이동은 Ajax 통신을 사용하며 page로 선언된 div 태그의 id 값을 링크의 href 속성 값(#pageId)으로 적용하여 사용 가능하다. (한 HTML 내에 여러 page가 선언되어 있을 경우 제일 상단의 page를 첫 화면으로 인식한다.)
페이지 내부 이동은 Ajax 방식을 기본으로 하기 때문에 연속적으로 여러 번 사용하면 DOM 객체를 제대로 못 불러 올 경우가 있으므로 외부 페이지 이동을 권장한다.

페이지 외부 이동
Ajax로 호출된 HTML의 page 영역만 가져오기 때문에 호출된 페이지에서 사용하는 JavaScript, CSS 등은 호출을 한 HTML 내에 존재해야 한다.
Ajax 통신을 사용하고 싶지 않은 경우 Internal / External UX Component를 참조하여 변경 가능하다.

mobileinit 이벤트와 기본 환경 설정
$(document).bind("mobileinit", function(){
//apply overrides here
});
<script src="jquery.js"></script>
<script src="custom-scripting.js"></script>
<script src="jquery-mobile.js"></script>
$.mobile 객체를 통해 재설정이 가능한 주요 기본 환경설정
| 기본 환경설정 | 설명 |
|---|---|
| loadingMessage (string, default: “loading”) | 페이지가 로딩될 때 나타나는 텍스트를 설정한다. ‘false’로 설정하면 로딩 메시지가 나타나지 않는다. |
| pageLoadErrorMessage (string, default: “Error Loading Page”) | Ajax 방식의 페이지 이동에서 페이지를 로드하지 못했을 경우 나타나는 에러 메시지의 텍스트를 설정한다. |
| defaultDialogTransition (string, default: ‘pop’) | 다이얼로그에서 Ajax 방식을 통한 페이지 전환에 관여하는 기본 환경설정을 변경한다. defaultDialogTransition 옵션을 ‘none’으로 설정하면 화면전환 효과가 적용되지 않는다. |
| defaultPageTransition (string, default: ‘slide’) | Ajax 방식을 사용하는 페이지 전환에 관여하는 기본 환경설정을 변경한다. defaultPageTransition 옵션을 ‘none’으로 설정하면 화면전환 효과가 적용되지 않는다. |
| ajaxEnabled (boolean, default: true) | 모든 링크 이동이나 폼 전송은 기본적으로 Ajax 방식을 기반으로 하고 있다. Ajax가 아니라 일반 방식으로 페이지 이동을 처리하고 싶다면 이 값을 ‘false’로 지정한다. |
이벤트
전자정부 모바일 표준프레임워크는 스마트 기반 모바일 환경에 적합한 이벤트를 선별하여 제공한다. Touch, Mouse, Window 영역의 다양한 이벤트를 지원 가능 여부에 따라 선택적으로 이용하기 때문에 모바일 환경과 데스크톱(Desktop) 환경 모두에서 사용 가능하다. live() 또는 bind() 메서드를 이용하여 여러 이벤트를 함께 사용할 수 있다.
지원 터치 이벤트
| 터치 이벤트 | 설명 |
|---|---|
| tap | Touch가 감지되면 즉시 발생하는 이벤트이다. |
| taphold | tap을 일정 시간 이상 지속했을 때 발생하는 이벤트이다. |
| swipe | 30pixel 이상의 수평 방향이나 20pixel 이상의 수직 방향으로 드래그(drag) 되면 발생하는 이벤트이다. |
| swipeleft | swipe 이벤트가 왼쪽으로 일어났을 때 발생하는 이벤트이다. |
| swiperight | swipe 이벤트가 오른쪽으로 일어났을 때 발생하는 이벤트이다. |
지원 화면 방향 전환 및 스크롤 이벤트
| 화면 방향 전환 및 스크롤 이벤트 | 설명 |
|---|---|
| orientationChange | 기기의 방향이 수평 또는 수직으로 바뀌었을 때 발생하는 이벤트이다. orientationChange 이벤트가 지원되지 않을 경우에는 resize 이벤트가 자동으로 bind 된다. |
| scrollstart | 스크롤(scroll)이 시작되면 발생하는 이벤트이다. (iOS 기기는 스크롤 중에는 DOM 을 변경하지 않고 queue에 저장해두었다가 스크롤이 끝난 후에 변경한다.) |
| scrollstop | 스크롤이 끝나면 발생하는 이벤트이다. |
지원 페이지 이벤트
| 페이지 이벤트 | 설명 |
|---|---|
| pagebeforecreate | 페이지가 초기화되기 직전에 발생하며 페이지 로딩 시 가장 먼저 발생하는 이벤트이다. 페이지 생성 시에만 이벤트가 발생한다. |
| pagecreate | 페이지 초기화가 끝나면 발생하는 이벤트이다. 페이지 생성이 완료된 시점에만 이벤트가 발생한다. |
| pagebeforeshow | 화면전환이 일어나기 전, 즉 페이지가 보이기 전에 매번 발생하는 이벤트이다. |
| pageshow | 화면전환이 완료되었거나 페이지가 보인 후에 매번 발생하는 이벤트이다. |
| pagebeforehide | 화면전환이 일어나기 전, 즉 페이지가 숨겨지기 전에 매번 발생하는 이벤트이다. |
Visual Mouse event
| 페이지 이벤트 | 설명 |
|---|---|
| vmouseover | 터치 이벤트 또는 mouseover가 발생할 때 나타나는 이벤트이다. |
| vmousedown | 터치 이벤트 또는 mousedown이 발생할 때 나타나는 이벤트이다. |
| vmousemove | 터치 이벤트 또는 mousemove가 발생할 때 나타나는 이벤트이다. |
| vmouseup | 터치 이벤트 또는 mouseup이 발생할 때 나타나는 이벤트이다. |
메서드 & 유틸리티
| 메서드 | 설명 |
|---|---|
| $.mobile.changePage(method) | 프로그램 코드 상에서 페이지를 이동할 수 있도록 지원하는 메서드이다. 주로 화면전환, 페이지 로딩 등의 기능이 가능한 환경에서 링크 클릭이나 폼 전송을 할 때 내부적으로 사용된다. |
| $.mobile.loadPage(method) | 외부 페이지를 로드하고, DOM에 추가한다. 이 메서드는 첫 번째 인자로 URL이 올 때 changePage() 함수를 통해 내부적으로 호출된다. 이 함수는 현재 활성화된 페이지에는 영향을 주지 않고, 백그라운드에서 페이지를 로드 할 때 사용된다. |
| $.mobile.loading(“show”) | 페이지 로딩 메시지를 보여준다. |
| $.mobile.loading(“hide”) | 페이지 로딩 메시지를 숨긴다. |
업무처리 서비스는 업무 프로그램의 업무 로직을 담당하는 서비스로 업무 흐름제어, 에러 처리 등의 기능을 제공한다.
전자정부 표준프레임워크 기반의 시스템 개발 시 Exception 처리, 정확히는 Exception별 특정 로직(후처리 로직이라고 부르기도 함)이 흐를 수 있도록 하여 Exception에 따른 적절한 대응이 가능하도록 하고자 하는데 목적이 있다.
AOP의 도움을 받아 비즈니스 POJO와 분리되어 After throwing advice로 정의하였다.
AOP 관련 내용은 AOP 모듈을 참조하길 바란다.
Exception에 대해 이야기 하겠다.
Exception 발생 시 Exception 발생 클래스 정보와 Exception 종류가 중요하다.
Exception 발생 클래스 정보와 Exception 종류는 모두 후처리 로직의 대상일지 아닐지를 결정하는 데 사용된다.
public CategoryVO selectCategory(CategoryVO vo) throws Exception {
CategoryVO resultVO = categoryDAO.selectCategory(vo);
try {
....
// 넘어온 resultVO가 null 인 경우 EgovBizException 발생 (result.nodata.msg는 메세지 키에 해당됨)
if (resultVO == null)
throw processException("result.nodata.msg");
// 또는 throw processException("result.nodata.msg", 발생한 Exception );
return resultVO;
}
앞에서 언급했던 Exception 후처리 방식과 Exception은 아니지만 후처리 로직(leaveaTrace)을 실행할 하는 방식에 대해 설명하도록 하겠다.
간략하게 보면 Exception 후처리 방식은 AOP(pointCut ⇒ after-throw) ⇒ ExceptionTransfer.transfer() ⇒ ExceptionHandlerService ⇒ Handler 순으로 실행된다.
LeavaTrace는 AOP를 이용하는 구조가 아니고 Exception을 발생하지도 않는다. 단지 후처리 로직을 실행하도록 하고자 함에 목적이 있다.
실행 순서는 LeavaTrace ⇒ TraceHandlerService ⇒ Handler 순으로 실행한다.
먼저 Exception Handling에 대해 알아보도록 하자.
Exception 후처리와 leaveaTrace 설정을 위해서 샘플에서는 두 개의 xml 파일을 이용한다. (context-aspect.xml, context-common.xml)
먼저 Exception 후처리를 위한 부분을 보겠다.
Exception Handling을 위한 AOP 설정은 아래와 같다.
비즈니스 개발 시 패키지 구조는 바뀌기 때문에 Pointcut은 egov.sample.service.*Impl.*(..))을 수정하여 적용할 수 있다.
ExceptionTransfer의 property로 존재하는 exceptionHandlerService는 다수의 HandleManager를 등록 가능하도록 되어 있다.
여기서는 defaultExceptionHandleManager을 등록한 것을 볼 수 있다.
...
<aop:config>
<aop:pointcut id="serviceMethod"
expression="execution(* egov.sample.service.*Impl.*(..))" />
<aop:aspect ref="exceptionTransfer">
<aop:after-throwing throwing="exception"
pointcut-ref="serviceMethod" method="transfer" />
</aop:aspect>
</aop:config>
<bean id="exceptionTransfer" class="egovframework.rte.fdl.cmmn.aspect.ExceptionTransfer">
<property name="exceptionHandlerService">
<list>
<ref bean="defaultExceptionHandleManager" />
</list>
</property>
</bean>
<bean id="defaultExceptionHandleManager"
class="egovframework.rte.fdl.cmmn.exception.manager.DefaultExceptionHandleManager">
<property name="patterns">
<list>
<value>**service.*Impl</value>
</list>
</property>
<property name="handlers">
<list>
<ref bean="egovHandler" />
</list>
</property>
</bean>
<bean id="egovHandler"
class="egovframework.rte.fdl.cmmn.exception.handler.EgovServiceExceptionHandler" />
...
defaultExceptionHandleManager는 setPatterns(), setHandlers() 메소드를 가지고 있다. 상단과 같이 등록된 pattern 정보를 이용하여 Exception 발생 클래스와의 비교하여 ture인 경우 handlers에 등록된 handler를 실행한다.
패턴 검사 시 사용되는 pathMatcher는 AntPathMatcher를 이용하고 있다.
특정 pattern 그룹군을 만든후 patterns에 등록하고 그에 해당하는 후처리 로직을 정의하여 등록할 수 있는 구조이다.
먼저 클래스에 대한 이해가 필요하다.
앞단에서 간단하게 설명했지만 다시 정리 하자면 Exception 발생 시 AOP pointcut “After-throwing”에 걸려 ExceptionTransfer 클래스의 transfer가 실행된다.
transfer 메소드는 ExceptionHandlerManager의 run 메소드를 실행한다.
아래는 구현 예로 DefaultExceptionHandleManager 코드이다.
(구현 시 필수사항) 상위클래스는 AbsExceptionHandleManager 이고 인터페이스는 ExceptionHandlerService 이다.
구현되는 메소드는 run(Exception exception)인 것을 확인할 수 있다.
public class DefaultExceptionHandleManager extends AbsExceptionHandleManager implements ExceptionHandlerService {
@Override
public boolean run(Exception exception) throws Exception {
log.debug(" DefaultExceptionHandleManager.run() ");
// 매칭조건이 false 인 경우
if (!enableMatcher())
return false;
for (String pattern : patterns) {
log.debug("pattern = " + pattern + ", thisPackageName = " + thisPackageName);
log.debug("pm.match(pattern, thisPackageName) =" + pm.match(pattern, thisPackageName));
if (pm.match(pattern, thisPackageName)) {
for (ExceptionHandler eh : handlers) {
eh.occur(exception, getPackageName());
}
break;
}
}
return true;
}
}
시나리오 : CustomizableHandler 클래스를 만들어 보고 sample 패키지에 있는 Helloworld 클래스 Exception 시에 CustomizableHandler를 실행한다.
먼저 CustomHandler 클래스를 아래와 같이 만든다.
ExceptionHandleManager 에서는 occur 메소드를 실행한다.
Handler 구현체는 반드시 (필수사항) ExceptionHandler 인터페이스를 구현해야 한다.
public class CustomizableHandler implements ExceptionHandler {
protected Log log = LogFactory.getLog(this.getClass());
public void occur(Exception ex, String packageName) {
log.debug(" CustomHandler run...............");
try {
log.debug(" CustomHandler 실행 ... ");
} catch (Exception e) {
e.printStackTrace();
}
}
}
CustomizableHandler의 등록을 해보도록 하겠다.
여기서 주의해야 하는 부분은 patterns에 sample 패키지에 있는 Helloworld 클래스를 지정해주어야 한다는 것이다.
<bean id="exceptionTransfer" class="egovframework.rte.fdl.cmmn.aspect.ExceptionTransfer">
<property name="exceptionHandlerService">
<list>
<ref bean="customizableExceptionHandleManager" />
</list>
</property>
</bean>
<bean id="customizableExceptionHandleManager"
class="egovframework.rte.fdl.cmmn.exception.manager.DefaultExceptionHandleManager">
<property name="patterns">
<list>
<value>**sample.Helloworld</value>
</list>
</property>
<property name="handlers">
<list>
<ref bean="customizableHandler1" />
<ref bean="customizableHandler2" />
<ref bean="customizableHandler3" />
</list>
</property>
</bean>
<bean id="customizableHandler1" class="sample.CustomizableHandler" />
<bean id="customizableHandler2" class="sample.CustomizableHandler" />
<bean id="customizableHandler3" class="sample.CustomizableHandler" />
이런식으로 여러개의 Handler를 등록해줄 수 있다.
Exception이거나 Exception이 아닌 경우에 Trace 후처리 로직을 실행시키고자 할 때 사용한다.
설정하는 기본적인 구조는 Exception 후처리하는 방식과 같다. 설정 파일은 context-common.xml 이다.
DefaultTraceHandleManager에 TraceHandler를 등록하는 형태로 설정된다.
...
<bean id="leaveaTrace" class="egovframework.rte.fdl.cmmn.trace.LeaveaTrace">
<property name="traceHandlerServices">
<list>
<ref bean="traceHandlerService" />
</list>
</property>
</bean>
<bean id="traceHandlerService" class="egovframework.rte.fdl.cmmn.trace.manager.DefaultTraceHandleManager">
<property name="patterns">
<list>
<value>*</value>
</list>
</property>
<property name="handlers">
<list>
<ref bean="defaultTraceHandler" />
</list>
</property>
</bean>
<bean id="antPathMatcher" class="org.springframework.util.AntPathMatcher" />
<bean id="defaultTraceHandler"
class="egovframework.rte.fdl.cmmn.trace.handler.DefaultTraceHandler" />
...
package egovframework.rte.fdl.cmmn.trace.handler;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
public class DefaultTraceHandler implements TraceHandler {
public void todo(Class clazz, String message) {
//수행하고자 하는 처리로직을 넣는 부분...
System.out.println(" log ==> DefaultTraceHandler run...............");
}
}
사용 방법을 다시 상기해보면 아래와 같다.
메세지 키(message.trace.msg)를 이용하여 메세지 정보를 넘겨 Handler를 실행한다.
public CategoryVO selectCategory(CategoryVO vo) throws Exception {
CategoryVO resultVO = categoryDAO.selectCategory(vo);
try {
//강제로 발생한 ArithmeticException
int i = 1 / 0;
} catch (ArithmeticException athex) {
//Exception을 발생하지 않고 후처리 로직 실행.
leaveaTrace("message.trace.msg");
}
return resultVO;
}
Spring Web Flow(SWF)는 웹 애플리케이션 내 페이지 흐름(flow)의 정의와 수행에 집중하는 Spring 프레임워크 웹 스택의 컴포넌트이다.
시스템은 다른 위치에서 재사용될 수 있는 자족적 모듈처럼 웹 애플리케이션의 논리적 흐름(flow)을 획득하는 것을 허용한다.
이러한 흐름(flow)은 비즈니스 프로세스의 구현을 통해 단일 사용자를 안내하고 단일 사용자 대화를 표현한다.
흐름(flow)은 종종 HTTP 요청을 처리하고 상태를 가지며, 트랜잭션 특성을 보이고 동적이고/이거나 장시간 구동될 수 있다.
Spring Web Flow는 추상화의 좀 더 높은 레벨에 존재하고 Struts, Spring MVC, Portlet MVC, 그리고 JSF와 같은 기본 프레임워크 내에서 자족적인 페이지 흐름(flow) 엔진(page flow engine)처럼 통합된다.
SWF는 선언적이고 높은 이식성을 가지며 뛰어난 관리능력을 가지는 형태로 명시적으로 애플리케이션의 페이지 흐름(flow)을 획득하는 기능을 제공한다.
Spring Web Flow는 여타의 API에 대한 몇 가지 요구 의존성을 가진 자족적인 page flow engine처럼 구조화되었다. 모든 의존성은 주의 깊게 관리된다.
대부분의 사용자들은 좀 더 큰 웹 애플리케이션 개발 프레임워크 내 컴포넌트로 SWF를 끼워 넣을 것이다.
SWF는 요청 맵핑과 응답 표현을 다루기 위한 호출 시스템을 기대하는 컨트롤러 기술에 집중한다.
이 경우, 이러한 사용자는 환경을 위한 가는(thin) 통합 조각에 의존할 것이다.
예를 들어, Servlet 내 수행 흐름(flow)은 SWF에 대한 요청에 대한 할당(dispatch)과 SWF view 선택을 책임지는 표현을 다루는 Spring MVC 통합을 사용한다.
Spring과 마찬가지로, Spring Web Flow는 필요한 부분만 선택적으로 사용할 수 있는 계층화된(layered) 프레임워크로 패키징되어 있다.
SWF의 중요한 이득은 어떤 환경에서도 수행될 수 있는 자족적인 컨트롤러의 모듈을 재사용하여 정의할 수 있도록 하는 것이다.
구체적인 내용을 살펴보기 전에 Hello World를 실행해 보자.
Spring Web Flow의 기본 샘플로 Spring Source에서는 Hotel Booking 을 제공하고 있다.
우리는 Spring Web Flow 레퍼런스 문서를 기준으로 하고 샘플인 Hotel Booking 을 참고하는 형태로 설명하도록 하겠다.
Hotel Booking 샘플 데모 : 🌏 http://richweb.springframework.org/swf-booking-faces/spring/intro
처음으로 접하므로 여기서는 Hello World를 찍어 보면서 실행하는 것을 살펴보도록 하겠다.
Hello World는 두 가지 버전으로 입력되는 값이 없이 단지 Hello, Web Flow 화면을 호출하는 것과 입력값을 가지고 분기 처리 등 서비스 메소드를 실행 후 결과를 화면으로 보여주는 버젼으로 나누어 설명하겠다. 실행하여 보고자 하는 화면 결과는 아래와 같다.

Spring Web Flow는 사용자와 Service를 제공하는 서버 간의 대화하듯한 화면의 이동을 정의하는 것이다.
SWF(Spring Web Flow)는 사용자와 화면 간의 대화 형태로 웹 대화형 시나리오를 중심으로 접근한다.

webContent/WEB-INF 아래 web.xml을 아래와 같이 작성한다.
contextConfigLocation의 값으로 /WEB-INF/config/web-application-config.xml을 설정한다.
servlet으로 org.springframework.web.servlet.DispatcherServlet를 등록하고 /spring/* URL 정보를 매핑해준다.
<?xml version="1.0" encoding="ISO-8859-1"?>
<web-app xmlns="http://java.sun.com/xml/ns/j2ee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd"
version="2.4">
<!-- Spring Web 어플리케이션을 위한 메인 설정파일을 등록한다. -->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>
/WEB-INF/config/web-application-config.xml
</param-value>
</context-param>
<!-- Spring Web 어플리케이션 컨테스트를 로딩한다. -->
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<!-- Spring Web 어플리케이션의 맨 앞단 Controller(DispatcherServlet) 를 등록한다. -->
<servlet>
<servlet-name>Spring MVC Dispatcher Servlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value></param-value>
</init-param>
<load-on-startup>0</load-on-startup>
</servlet>
<!-- 모든 spring 요청에 대한되는 request를 DispatcherServlet 와 매핑하여 처리 할 수 있도록 한다. -->
<servlet-mapping>
<servlet-name>Spring MVC Dispatcher Servlet</servlet-name>
<url-pattern>/spring/*</url-pattern>
</servlet-mapping>
<welcome-file-list>
<welcome-file>index.html</welcome-file>
</welcome-file-list>
</web-app>
Spring MVC와 Spring Web Flow를 위한 설정 파일은 아래와 같다.

먼저 web-application-config.xml를 살펴보겠다.
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-2.5.xsd">
<!-- 어플리케이션 소스를 스캔하여 로딩 하도록 한다. -->
<context:component-scan base-package="org.egovframe.swf.sample.service" />
<!-- 편의를 위하여 Spring MVC 설정과 Spring Web Flow 를 위한 설정을 별도록 분리하여 가져오도록 한다.-->
<import resource="webmvc-config.xml" />
<import resource="webflow-config.xml" />
</beans>
Spring MVC를 위한 설정 파일
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.5.xsd">
<!--
flowRegistry에 등록된 flow와 요청되는 path와 매핑해주는 역할을 수행한다.
예제에선 요청되는 .../swfHelloWorld/spring/sample/hello URL 정보를 이용하여 flow 내에서 sample/hello ID로 찾음.
-->
<bean class="org.springframework.webflow.mvc.servlet.FlowHandlerMapping">
<property name="order" value="0" />
<property name="flowRegistry" ref="flowRegistry" />
</bean>
<bean
class="org.springframework.web.servlet.mvc.support.ControllerClassNameHandlerMapping">
<property name="order" value="1" />
<property name="defaultHandler">
<!-- UrlFilenameViewController 는 spring/start 으로 접근하는 path 정보를 이용하여
View 이름을 추출하여 View를 반환하게 된다. 여기서는 tiles의 view를 반환하게 된다. -->
<bean class="org.springframework.web.servlet.mvc.UrlFilenameViewController" />
</property>
</bean>
<!--
Controller에 의해 반환된 View 명을 tiles로 보내 tiles에 미리 정의된 화면을 보여주도록 한다.
-->
<bean id="tilesViewResolver" class="org.springframework.js.ajax.AjaxUrlBasedViewResolver">
<property name="viewClass"
value="org.springframework.webflow.mvc.view.FlowAjaxTilesView" />
</bean>
<!-- tiles 설정 정보를 정의한다. -->
<bean id="tilesConfigurer" class="org.springframework.web.servlet.view.tiles2.TilesConfigurer">
<property name="definitions">
<list>
<value>/WEB-INF/layouts/layouts.xml</value>
<value>/WEB-INF/views.xml</value>
<value>/WEB-INF/sample/views.xml</value>
<value>/WEB-INF/sample/hello/views.xml</value>
</list>
</property>
</bean>
<!-- Dispatches requests mapped to POJO @Controllers implementations-->
<bean class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter" />
<!--
Dispatches requests mapped to
org.springframework.web.servlet.mvc.Controller implementations
-->
<bean class="org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter" />
<!--
requests에 맞는 등록된 FlowHandler 구현부를 연결해준다.
-->
<bean class="org.springframework.webflow.mvc.servlet.FlowHandlerAdapter">
<property name="flowExecutor" ref="flowExecutor" />
</bean>
<!-- Custom FlowHandler for the hello flow-->
<bean name="sample/hello" class="org.egovframe.web.HelloFlowHandler" />
</beans>
Web Flow 관련된 설정 파일
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:webflow="http://www.springframework.org/schema/webflow-config"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
http://www.springframework.org/schema/webflow-config
http://www.springframework.org/schema/webflow-config/spring-webflow-config-2.0.xsd">
<webflow:flow-executor id="flowExecutor" />
<!-- flow 를 정의한 파일을 가져와 flow registry 구성한다. -->
<webflow:flow-registry id="flowRegistry"
flow-builder-services="flowBuilderServices" base-path="/WEB-INF">
<webflow:flow-location-pattern value="/**/*-flow.xml" />
</webflow:flow-registry>
<!-- Web Flow views 에 커스터마이징 할 수 있도록 확장하여 사용한다. -->
<webflow:flow-builder-services id="flowBuilderServices"
view-factory-creator="mvcViewFactoryCreator" conversion-service="conversionService"
development="true" />
<!-- Web Flow 에서 tiles 를 사용할 수 있도록 설정한다. -->
<bean id="mvcViewFactoryCreator"
class="org.springframework.webflow.mvc.builder.MvcViewFactoryCreator">
<property name="viewResolvers" ref="tilesViewResolver" />
</bean>
</beans>
상세 : Web Flow views 에 커스터마이징 할 수 있도록 확장하여 사용한다
URL : http://localhost:8080/swfHelloWorld 으로 처음 접근할 때 index.html 파일이 열리게 된다.
index.html
<html>
<head>
<meta http-equiv="Refresh" content="0; URL=spring/start">
</head>
</html>
위에서 보는 것처럼 “spring/start” URL을 호출한다.
spring/start 에 해당하는 화면은 먼저 설정된 tiles 설정 정보에서 찾는다.
<tiles-definitions>
<definition name="start" extends="standardLayout">
<put-attribute name="body" value="/WEB-INF/main.jsp" />
</definition>
</tiles-definitions>
tiles 관련된 것은 http://tiles.apache.org/를 참조하시길 바랍니다.
등록된 tiles 설정 파일은 앞 설정에서 나왔다. 다시 보면
...
<!-- tiles 설정 정보를 정의한다. -->
<bean id="tilesConfigurer" class="org.springframework.web.servlet.view.tiles2.TilesConfigurer">
<property name="definitions">
<list>
<value>/WEB-INF/layouts/layouts.xml</value>
<value>/WEB-INF/views.xml</value>
<value>/WEB-INF/sample/views.xml</value>
<value>/WEB-INF/sample/hello/views.xml</value>
</list>
</property>
</bean>
...
다시 돌아와서 Hello , Web Flow 를 화면에 찍어 보도록 하겠다.
Web Flow로 해당 화면의 흐름을 작성한 예를 보자.
hello-flow.xml
<?xml version="1.0" encoding="UTF-8"?>
<flow xmlns="http://www.springframework.org/schema/webflow"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/webflow
http://www.springframework.org/schema/webflow/spring-webflow-2.0.xsd">
<view-state id="hello">
<transition on="say" to="helloworld" />
</view-state>
<view-state id="helloworld">
<transition on="return" to="return" />
</view-state>
<end-state id="return" view="externalRedirect:servletRelative:/start" />
</flow>
자세한 설명은 flow 정의에서 다루고 있다.
간단하게 보면 view-state, end-state로 나눠져 있는 것을 볼 수 있다. 처음으로 존재하는 view-state는 시작점이라고 생각해도 무방하다. 또한 문자 그대로 end-state는 마지막점이다. hello 화면이 맨 처음 나오고 거기서 helloworld 화면이 보이고 다음은 return이라는 마지막 실행을 하는 것이다.
view-state 안쪽의 transition는 화면에서 클릭하여 이동하게 하는 버튼의 실행이라고 할 수 있다. 여기선 say를 눌러서 실행하면 helloworld 라는 view-state로 이동하는 것이다. 마찬가지로 return을 누르면 externalRedirect:servletRelative:/start으로 이동하는 것이다.
view-state에서 별도의 view를 정의하지 않은 경우 id를 가지고 view를 가져오게 된다. 여기서는 hello 라는 id가 곧 view 명이 되게 된다.
default는 flow.xml과 같은 디렉토리에 있는 화면소스(JSP, xhtml, 등)을 찾게 된다. 여기선 tiles로 정의된 부분을 참조한다.
…/hello/views.xml 파일 내용을 살펴보면,
...
<definition name="hello" extends="standardLayout">
<put-attribute name="body" value="/WEB-INF/sample/hello/hello.jsp" />
</definition>
...
로 화면에 해당되는 hello.jsp 를 가져오는 것을 확인할 수 있다.
그렇다면 transition은 화면에서 발생한 이벤트와 매핑을 할까? hello.jsp 소스를 잠시 보겠다.
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
<head>
<title>Welcome to Spring Web Flow</title>
</head>
<body>
<h1>Welcome to Spring Web Flow</h1>
<form:form id="start">
<input type="submit" name="_eventId_say" value="Click to say hello!" />
</form:form>
</body>
</html>

보는 바와 같이 form으로 둘러싸인 곳에 해답은 있다. <input type=“submit” name=“_eventId_say” …. /> 에서 name 을 보면 _eventId_say로 답을 찾을 수 있다.
_eventId가 답이다. say는 transition의 on과 같음을 확인할 수 있다. eventId 에 정의된 특정 위치의 문자열을 가지고 transition을 분석하다.
transition에 대한 내용은 flow 정의에서 자세히 살펴보길 바란다. eventId 가 “say”를 가지고 form 이 전달되면 flow 정의 flow 정의에 따라 transition을 찾고 그에 맞는 state로 넘어가게 된다.
결과는 별로의 값을 가지고 보여주는 화면은 아니고 단지 아래와 같은 화면을 보여주도록 되어 있다.

다음은 입력값이 있는 예를 살펴 보도록 하겠다.
먼저 flow 정의 파일인 hello2-flow.xml 을 보도록 하자. 실행 시나리오는 on-start ⇒ view-state ⇒ action-state ⇒ decision-stat ⇒ end-state 이다.
hello2-flow.xml
<?xml version="1.0" encoding="UTF-8"?>
<flow xmlns="http://www.springframework.org/schema/webflow"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/webflow
http://www.springframework.org/schema/webflow/spring-webflow-2.0.xsd">
<on-start>
<evaluate expression="helloService.sayMessage()" result="flowScope.message" />
</on-start>
<view-state id="hello2" model="message">
<binder>
<binding property="str" required="true" />
</binder>
<transition on="proceed" to="actionHello" />
<transition on="return" to="return" />
</view-state>
<action-state id="actionHello">
<evaluate expression="helloService.addHello(message)" />
<transition on="yes" to="moreDecision" />
<transition on="no" to="hello" />
</action-state>
<decision-state id="moreDecision">
<if test="helloService.getDecision(message)" then="helloworld2" else="return" />
</decision-state>
<view-state id="helloworld2">
<transition on="return" to="return" />
</view-state>
<end-state id="return" view="externalRedirect:servletRelative:/start" />
</flow>
</xml>
보여 주고자 하는 것은 hello2 화면(view-state)에서 입력 데이터를 객체에 바인딩하고, helloService 서비스 객체를 통해 addHello 메소드 실행, 그 후 결과에 따라 분기문(decision-state)을 통과하여 helloworld2 화면으로 가는 것이다.
간략하게 설명하면, on-start는 flow를 처음 실행할 때 선행하여 실행된다. 여기서는 helloService의 sayMessage를 실행하여 flowScope 내의 message 객체로 저장한다.
HelloService .java
...
@Service("helloService")
public class HelloService implements Iservice {
public Message sayMessage() {
return new Message();
}
...
}
flow가 시작할 때 첫 번째로 만나는 view-state는 시작점으로 인식한다. 따라서 view-state “hello2”는 시작점에 해당한다.
hello2.jsp 를 화면에 보여주는데 앞단의 예제와 같다. Spring MVC의 tiles를 이용하여 보여주게 된다.
views.xml
...
<definition name="hello2" extends="standardLayout">
<put-attribute name="body" value="inhello2.body" />
</definition>
<definition name="inhello2.body" template="/WEB-INF/sample/hello2/main.jsp">
<put-attribute name="helloSection" value="/WEB-INF/sample/hello2/hello.jsp" />
</definition>
...
hello.jsp
...
<form:form method="post" >
<p>
to Who : <input type="text" id="str" name="str" value=" World ~*"/>
<script type="text/javascript">
Spring.addDecoration(new Spring.ElementDecoration({
elementId : "str",
widgetType : "dijit.form.ValidationTextBox",
widgetAttrs : { promptMessage : "for who ? ", required : true }}));
</script>
<br>
</p>
<input id="proceed" type="submit" class="button"
name="_eventId_proceed" value="say">
<script type="text/javascript">
Spring.addDecoration(new Spring.ValidateAllDecoration({elementId:'proceed', event:'onclick'}));
</script>
<input type="submit" class="button"
name="_eventId_return" value="index">
</form:form>
...
상단의 화면은 아래 hello2-flow.xml 내의 view-state와 매핑된다.
여기서 봐야 할 부분은 화면 내의 str 이름의 input 데이터를 message라는 객체로 바인딩하는 부분인다.
...
<view-state id="hello2" model="message">
<binder>
<binding property="str" required="true" />
</binder>
<transition on="proceed" to="actionHello" />
<transition on="return" to="return" />
</view-state>
...
이벤트에 해당하는 proceed 버튼을 클리하면 다음 state로 이동하게 된다.
...
<transition on="proceed" to="actionHello" />
...
actionHello은 아래와 같다. 하는 기능은 helloService 객체의 addHello 메소드 호출이다.
...
<action-state id="actionHello">
<evaluate expression="helloService.addHello(message)" />
<transition on="yes" to="moreDecision" />
<transition on="no" to="hello" />
</action-state>
...
addHello 메소드는 아래와 같다. 반환되는 값이 boolean인 것을 주목할 필요가 있다. 리턴되는 boolean 값은 transition의 yes, no 와 매핑된다.
...
public boolean addHello(Message msg){
try{
msg.setStr("Hello,"+msg.getStr());
}catch (Exception e) {
return false;
}
return true;
}
...
다음 나오는 decision-state는 아래와 같이 분기문의 기능을 수행한다.
...
<decision-state id="moreDecision">
<if test="helloService.getDecision(message)" then="helloworld2" else="return" />
</decision-state>
...
helloworld2 화면으로 이동하게 되면 아래와 같은 jsp 소스를 확인할 수 있다. message 객체의 str 값을 EL 을 이용하여 ${message.str} 으로 보여주고 있다.
<%@ taglib prefix="form"
uri="http://www.springframework.org/tags/form" %>
<h2>Hello Message?</h2>
<form:form>
<b>Step two :</b>
<fieldset>
<div class="field">
<div class="label">
<label>${message.str}</label>
</div>
</div>
<div class="buttonGroup">
<input type="submit" class="button" name="_eventId_return" value="index">
</div>
</fieldset>
</form:form>
화면을 다시 보면

say 버튼을 누르면,

Hello , 뒤에 넣었던 문장이 붙어서 나오게 된다.
Spring Web Flow를 사용하기 위한 Web 개발 환경에 대한 세팅을 설명한다.
Spring Web Flow의 Flow 정의를 위한 XML 문서는 아래와 같은 Schema를 갖는다.
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:webflow="http://www.springframework.org/schema/webflow-config"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
http://www.springframework.org/schema/webflow-config
http://www.springframework.org/schema/webflow-config/spring-webflow-config-2.0.xsd">
<!-- Setup Web Flow here -->
</beans>
Spring Web Flow를 사용하려면 FlowRegistry FlowExecutor를 설정해야 한다.
FlowRegistry는 등록될 시나리오에 따라 작성된 flow xml 을 가져오는 역할[1]을 수행한다. FlowExecutor는 등록된 flow 설정 xml을 실행[2]한다. 차후 Spring MVC 와 결합하여 Web Flow 시스템이 실행되는 부분에 대해 다루겠다.
<!-- [1] Flow 설정 파일 등록-->
<webflow:flow-registry id="flowRegistry">
<webflow:flow-location path="/WEB-INF/flows/booking/booking.xml" />
</webflow:flow-registry>
<!-- [2] Flow 실행의 중추 역할을 하는 서비스 제공-->
<webflow:flow-executor id="flowExecutor" />
flow-registry는 아래 보는 것과 같이 설정할 수 있다.
<!-- [1]기본은 이름-flow.xml이지만, 직접 지정할 수도 있다. -->
<webflow:flow-location path="/WEB-INF/flows/booking/booking.xml" />
<!-- [2]id로 식별이 가능하도록 할 수도 있다 -->
<webflow:flow-location path="/WEB-INF/flows/booking/booking.xml" id="bookHotel" />
<!-- [3]메타 정보도 등록할 수 있다. -->
<webflow:flow-location path="/WEB-INF/flows/booking/booking.xml">
<flow-definition-attributes>
<attribute name="caption" value="Books a hotel" />
</flow-definition-attributes>
</webflow:flow-location>
<!-- [4]ANT 패턴을 지정할 수도 있다. -->
<webflow:flow-location-pattern value="/WEB-INF/flows/**/*-flow.xml" />
<!--
[5]기본 앞 첨자 경로를 지정해서 위치를 조합해서 사용할 수도 있다.
Flow 정의 파일은 모듈화를 높이기 위해서 관련 있는 폴더에 각각 위치해 있는게 가장 좋다. -->
<webflow:flow-registry id="flowRegistry" base-path="/WEB-INF">
<webflow:flow-location path="/hotels/booking/booking.xml" />
</webflow:flow-registry>
<!-- [6]Flow 상속 구조 구성 가능 -->
<!-- my-system-config.xml -->
<webflow:flow-registry id="flowRegistry" parent="sharedFlowRegistry">
<webflow:flow-location path="/WEB-INF/flows/booking/booking.xml" />
</webflow:flow-registry>
<!-- shared-config.xml -->
<webflow:flow-registry id="sharedFlowRegistry">
<!-- Global flows shared by several applications -->
</webflow:flow-registry>
flow-registry는 아래 보는 것과 같이 설정할 수 있다.
flow-registry에서 flow-builder-services는 flow를 구축하는 데 사용되는 서비스나 설정 등을 커스터마이징할 수 있다. 지정하지 않는 경우에는 기본 서비스가 사용된다.
<webflow:flow-registry id="flowRegistry" flow-builder-services="flowBuilderServices">
<webflow:flow-location path="/WEB-INF/flows/booking/booking.xml" />
</webflow:flow-registry>
<webflow:flow-builder-services id="flowBuilderServices" />
SWF 시스템에서 사용하는 ConversionService를 커스터마이징. Flow 실행 동안에 필요한 경우 특정 타입을 다른 타입으로 변환해 줌(propertyEditor 성격)
ExpressionParser 커스터마이징하는데 사용. 기본은 Unified EL이 사용되며, 사용하는 영역은 classpath 이다. 다른 ExpressionParser로는 OGNL이 사용 됨.
ViewFactoryCreator를 커스터마이징. 디폴트 ViewFactoryCreator는 JSP, Velocity, Freemaker 등을 화면에 보여주게 해주는 Spring MVC ViewFactories로 되어 있음.
Flow 개발 모드 설정. true인 경우, Flow 정의가 변경되면 hot-reloading 적용(message bundles 과 같은 리소스 포함).
<webflow:flow-builder-services id="flowBuilderServices"
conversion-service="conversionService"
expression-parser="expressionParser"
view-factory-creator="viewFactoryCreator" />
<bean id="conversionService" class="..." />
<bean id="expressionParser" class="..." />
<bean id="viewFactoryCreator" class="..." />
Flow 실행의 Lifecycle에 관련된 리스너(listener)는 flow-execution-listeners를 이용하여 등록한다.
<flow-execution-listeners>
<listener ref="securityListener" />
<listener ref="persistenceListener" />
</flow-execution-listeners>
또한 특정 흐름에 대해서만 적용 가능하다.
<listener ref="securityListener" criteria="securedFlow1,securedFlow2"/>
<flow-execution-repository max-executions="5" max-execution-snapshots="30" />
사용자 세션 당 생성될 수 있는 Flow 실행 개수 지정
Flow 실행 당 받을 수 있는 이력 snapshot 개수 지정. snapshot을 사용하지 못하게 하려면, 0으로 지정. 제한이 없게 하려면 -1로 설정.
Spring Web Flow를 사용하여 웹을 개발할 때 Spring MVC와 연동하여 개발할 수 있다. 이를 위해 Spring MVC 연동 모듈 등을 설정해야 한다. 여기서는 booking-mvc sample( 실행데모(faces이지만 시나리오는 같음) )을 기준으로 설정하겠다.
Spring MVC 와의 연동을 위해 우리는 web.xml 안에 있는 DispatcherServlet 설정을 보도록 하겠다.
Spring MVC를 구성하는 첫 단계는 web.xml에 DispatcherServlet을 구성하는 것이다. DispatcherServlet은 웹 애플리케이션별 하나를 등록한다.
이 예제에서는 /spring/으로 시작하는 모든 요청을 받도록 설정하고 있다. init-param을 사용해 contextConfigLocation을 설정하고 있다.
web.xml
<servlet>
<servlet-name>Spring MVC Dispatcher Servlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/web-application-config.xml</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>Spring MVC Dispatcher Servlet</servlet-name>
<url-pattern>/spring/*</url-pattern>
</servlet-mapping>
DispatcherServlet은 애플리케이션 자원에 대한 요청과 핸들러를 매핑시켜 준다. Flow도 핸들러의 하나의 유형으로 처리된다.
먼저 FlowHandlerAdapter Bean을 정의하고 나서 property(flowExecutor)로 flowExecutor 빈을 설정함으로써 Spring MVC 내에서 Flow를 제어할 수 있도록 한다.
<!-- Enables FlowHandler URL mapping -->
<bean class="org.springframework.webflow.mvc.servlet.FlowHandlerAdapter">
<property name="flowExecutor" ref="flowExecutor" />
</bean>
이 설정은 Dispatcher가 애플리케이션 자원 경로를 flow registry에 등록된 Flow로 매핑할 수 있도록 해준다. 예를 들어, /hotels/booking 요청은 hotels/booking 이란 Flow ID를 갖는 Flow에게 요청이 가게 된다. flow registry에서 해당 ID 못 찾게 되면, Dispatcher의 순서에 따라 다음 핸들러 매핑에서 찾고, 없다면 “noHandlerFound”(?) 응답이 반환되게 된다.
조건에 맞는 flow가 매핑되면 FlowHandlerAdapter는 새로운 flow 실행을 시작할 것인지 아니면 HTTP 요청에 담겨있는 정보를 기반으로 기존 실행을 계속할 것인지를 판단하게 된다.
자세한 내용은 SWF JavaDoc 에서 FlowHandlerAdapter 클래스를 참고하길 바란다.
FlowHandler는 HTTP 서블릿 환경에서 Flow가 실행을 커스터마이징할 수 있는 확장 영역이다. FlowHandlerAdapter에 의해 실행/사용되며, 아래와 같은 수행을 한다.
이러한 수행을 위한 메소드는 org.springframework.mvc.servlet.FlowHandler 인터페이스 형태로 되어 있다.
public interface FlowHandler {
public String getFlowId();
public MutableAttributeMap createExecutionInputMap(HttpServletRequest request);
public String handleExecutionOutcome(FlowExecutionOutcome outcome, HttpServletRequest request, HttpServletResponse response);
public String handleException(FlowException e, HttpServletRequest request, HttpServletResponse response);
}
FlowHandler를 구현 시, AbstractFlowHandler를 상속하면 된다. 모든 연산은 선택적이 되어서, 필요할 때만 구현하면 되며, 구현하지 않으면 기본 구현 내용(AbstractFlowHandler 내에 구현된)이 적용된다. 특히 다음과 같은 경우에는 구현을 고려할 수 있다.
가장 일반적인 스프링 MVC와의 상호 작용은, Flow가 종료됐을 때 @Controller로 재전송하는 방법이다. FlowHandler는 이를 특정 controller URL을 Flow 정의와 관련 없이 가능하도록 해준다. 예를 보자.
public class BookingFlowHandler extends AbstractFlowHandler {
public String handleExecutionOutcome(FlowExecutionOutcome outcome, HttpServletRequest request,
HttpServletResponse response) {
if (outcome.getId().equals("bookingConfirmed")) {
return "/booking/show?bookingId=" + outcome.getOutput().get("bookingId");
} else {
return "/hotels/index";
}
}
}
재정의된 handleExecutionOutcome 메소드에서는 Flow 결과로 나온 flow id 가 bookingConfirmed인 경우 특정 URL(/booking/show?bookingId=…)로 보낸다. flow id 가 bookingConfirmed 와 다른 경우 /hotels/index 에 대한되는 URL로 가게 된다.
커스텀 FlowHandler를 설치하려면,그냥 빈으로 등록하기만 하면 된다. 빈 이름은 반드시 적용하고자 하는 Flow의 id와 일치해야 한다
<bean name="hotels/booking" class="org.springframework.webflow.samples.booking.BookingFlowHandler"/>
이 설정을 통해서 /hotels/booking 자원에 대한 접근은 커스텀 핸들러인 BookingFlowHandler를 사용해서 hotels/booking이 실행되게 된다. booking flow 가 끝나는 시점에서 BookingFlowHandler의 handleExecutionOutcome이 실행되며 String(URL) 결과에 따라 적당한 controller로 재전송된다.
FlowExecutionOutcome이나 FlowException을 제어하는 FlowHandler는 제어를 한 후에 재전송하는 경로를 지정하는 String을 반환하게 된다. 이전 예에서 BookingFlowHandler는 bookingConfirmed 결과에 대해서는 booking/show로 재전송하고, 다른 결과에 대해서는 hotels/index 자원 URI로 반환하게 된다. 기본적으로 반환되는 자원의 위치는 현재 서블릿 매핑에 관계된다. 이는 flow handler가 상대 경로를 사용해서 애플리케이션 내에 있는 다른 컨트롤러로 redirect할 수 있게 해준다. 여기에 더해서 좀더 제어가 필요한 경우에 사용할 수 있는 명시적인 앞첨자를 제공한다.
이 앞첨자는 externalRedirect: 지시어와 함께 Flow 정의 내에서도 사용할 수 있다. 예를 들면, view=“externalRedirect:http://springframework.org”.
Web Flow 2는 따로 지정하지 않는다면 Flow 파일이 있는 디렉터리에 있는 파일과 선택된 뷰 식별자를 매핑해주게 된다. 기존 스프링 MVC+Web Flow 애플리케이션에서는 이미 외부 ViewResolver가 매핑 처리를 해주고 있다. 그러므로 기존 resolver를 계속 사용하고, 기존 Flow 뷰가 패키징된 방법이 변경되는 것을 피하기 위해서 다음처럼 설정하자.
<webflow:flow-registry id="flowRegistry" flow-builder-services="flowBuilderServices">
<webflow:location path="/WEB-INF/hotels/booking/booking.xml" />
</webflow:flow-registry>
<webflow:flow-builder-services id="flowBuilderServices" view-factorycreator="mvcViewFactoryCreator" />
<bean id="mvcViewFactoryCreator" class="org.springframework.webflow.mvc.builder.MvcViewFactoryCreator">
<property name="viewResolvers" ref="myExistingViewResolverToUseForFlows" />
</bean>
MvcViewFactoryCreator는 당신이 Spring MVC view 시스템(여기선 “myExistingViewResolverToUseForFlows”)을 Spring Web Flow 내에서 사용할 수 있도록 해준다. Booking Hotels 샘플에서는 아래와 같이 설정되어 있다(tilesViewResolver 를 이용할 수 있도록 되어 있다).
<bean id="mvcViewFactoryCreator" class="org.springframework.webflow.mvc.builder.MvcViewFactoryCreator">
<property name="viewResolvers" ref="tilesViewResolver"/>
</bean>
또한 useSpringBinding의 값을 true로 설정하면 Spring MVC의 고유한 BeanWrapper를 이용하여 데이터 바인딩을 할 수 있다.
flow가 view-state에 들어가서 잠시 멈추게 되면, 사용자를 해당 실행 URL로 재전송해서, 사용자 이벤트가 다시 시작되기를 기다리게 된다. 이번 절에서는 JSP, Velocity나 Freemarker처럼 템플릿 엔진에 의해서 생성된 HTML 기반 뷰에서 이벤트를 발생시키는 방법을 알아보자.
다음은 proceed와 cancle 이벤트를 발생시키는 같은 폼 내의 두 개 버튼을 보여준다.
<input type="submit" name="_eventId_proceed" value="Proceed" />
<input type="submit" name="_eventId_cancel" value="Cancel" />
버튼이 선택되면, SWF는 _eventId로 시작하는 요청 파라미터를 찾아서, 그 부분을 잘라내고 남은 문자열을 id로 사용하게 된다. _eventId_proceed는 proceed가 된다. 그러기 때문에 동일한 폼에서 다양한 여러 이벤트를 발생시킬 수 있다.
form이 submit될 때 proceed 이벤트가 발생하려면 다음처럼 하자.
<input type="submit" value="Proceed" />
<input type="hidden" name="_eventId" value="proceed" />
여기서는 _eventId 파라미터로 오는 값을 찾아서 event id로 해당 값을 사용하게 된다. 이런 방법은 form으로 전송할 수 있는 이벤트가 하나일 때만 생각해봐야 한다.
<a href="${flowExecutionUrl}&_eventId=cancel">Cancel</a>
매개변수 식별 순서는 “eventId” ⇒ “_eventId” ⇒ 없음 이다.
보안은 어플리케이션 에서 매우 중요한 이슈이다. Spring Security는 어플리케이션과 결합하여 여러 수준에서 보안을 책임지는 플랫폼의 기능을 수행한다. 여기서는 Web Flow에 적용되는 Spring Security에 대해 알아보겠다.
Flow 실행에 보안을 적용시키고 싶다면 다음 단계에 따르자.
secured 구성 요소는 접근하기 전에 권한 확인을 적용해 주며, Flow 실행 단계마다 한 번 이상은 나올 수 없다. Flow 실행에서 세 단계로 보안을 적용할 수 있다. Flow, state, transition에 보안 적용이 가능하다. 사용되는 문법은 동일하다. secured 구성요소는 보안이 적용되어야 하는 구성 요소 내에 위치하면 된다. 예를 들어 view state에 보안을 적용하고자 하면,
<view-state id="secured-view">
<secured attributes="ROLE_USER" />
...
</view-state>
attributes 속성은 ‘,’(콤마)로 구분해서 SS의 권한 속성을 리스트로 입력할 수 있다. 이 속성은 대부분 허가된 보안 롤(role)을 명시하게 된다. 스프링 시큐리티 접근 결정 매니저(access decision manager)에 의해 이 속성에 입력한 값과 사용자가 가지고 있는 값을 비교한다.
<secured attributes="ROLE_USER" />
기본적으로, 롤 기반 접근 결정 관리자를 사용하여 사용자가 접근할 수 있는지 확인한다. 만약 애플리케이션이 권한 룰을 사용하지 않는다면 이 부분을 오버라이딩할 필요가 있다.
두 가지 유형의 일치 유형 제공: any, all.
<secured attributes="ROLE_USER, ROLE_ANONYMOUS" match="any" />
이 속성은 필수가 아니다. 정의하지 않으면 기본 값은 any다.
Web Flow 설정에 추가한다. SecurityFlowExecutionListener가 Web Flow 설정에 정의되어 있어야 플로우 실행기(executor)에 적용된다.
<webflow:flow-executor id="flowExecutor"
flow-registry="flowRegistry">
<webflow:flow-execution-listeners>
<webflow:listener ref="securityFlowExecutionListener" />
</webflow:flow-execution-listeners>
</webflow:flow-executor>
<bean id="securityFlowExecutionListener"
class="org.springframework.webflow.security.SecurityFlowExecutionListener" />
보안 설정에 의해서 접근이 거절되면, AccessDeniedException이 발생한다. 기본으로 롤 기반 의사결정이 이루어 지지만, 커스텀 의사결정 관리자를 지정할 수 있다.
<bean id="securityFlowExecutionListener" class="org.springframework.webflow.security.SecurityFlowExecutionListener">
<property name="accessDecisionManager" ref="myCustomAccessDecisionManager" />
</bean>
http와 authentication-provider로 정의하면 된다.
<security:http auto-config="true">
<security:form-login login-page="/spring/login"
login-processing-url="/spring/loginProcess" default-target-url="/spring/main"
authentication-failure-url="/spring/login?login_error=1" />
<security:logout logout-url="/spring/logout" logout-success-url="/spring/logout-success" />
</security:http>
<security:authentication-provider>
<security:password-encoder hash="md5" />
<security:user-service>
<security:user name="keith" password="417c7382b16c395bc25b5da1398cf076" authorities="ROLE_USER,ROLE_SUPERVISOR" />
<security:user name="erwin" password="12430911a8af075c6f41c6976af22b09" authorities="ROLE_USER,ROLE_SUPERVISOR" />
<security:user name="jeremy" password="57c6cbff0d421449be820763f03139eb" authorities="ROLE_USER" />
<security:user name="scott" password="942f2339bf50796de535a384f0d1af3e" authorities="ROLE_USER" />
</security:user-service>
</security:authentication-provider>
필터 설정.(SS 기본)
<filter>
<filter-name>springSecurityFilterChain</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
<filter-name>springSecurityFilterChain</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
대부분의 애플리케이션은 여러 방법으로 데이터에 접근한다. 여러 사용자가 공유하는 데이터를 동시에 수정한다. 따라서 트랜잭션 데이터 접근 속성이 필요하다. 관계형 데이터 집합을 도메인 객체로 변환하여 애플리케이션 처리를 도와준다. Web Flow는 “Flow가 관리하는 영속성”(flow managed persistence)을 제공하여 Flow가 객체 영속성 문맥을 만들고, commit 하고, 닫을 수 있도록 한다. Web Flow는 하이버네이트와 JPA 객체 영속화 기술과 연동한다.
Flow-관리 영속성과 별도로 PesistenceContext 관리를 애플리케이션의 서비스 계층에서 완전히 캡슐화하는 패턴이 있다. 이런 경우 Web 계층은 영속화에 관여하지 않는다. 그 대신 서비스 계층으로 념주겨거나 반환받은 detached object를 가지고 동작한다. 이번 장은 Flow-관리 영속성에 초점을 맞추고 이 기능을 언제 어떻게 사용하는지 살펴보겠다.
이 패턴은 Flow 시작 시에 flowScope으로 PersistenceContext를 생성해 준다. 이 persistence context는 Flow 실행 과정 동안에 데이터 접근을 하는데 사용하며, Flow가 종료될 때 persistent entity에서 변경된 내용을 반영(commit) 한다. 이 패턴은 대부분 다중 사용자에 의해서 동시에 수정되는 데이터의 정합성을 보호하고자 optimistic locking 전략과 함께 사용된다.
저장이나 재시작 능력이 필요 없다면, Flow 상태를 표준 HTTP 세션 기반 저장 방법이 충분하다. 이 때 커밋 전 세션 만료나 종료(termination)는 잠재적으로 변경 사항을 손실할 수 있다. FlowScoped PersistenceContext 패턴을 사용하려면 먼저 persistence-context로 Flow를 식별하게 해야 한다.
<?xml version="1.0" encoding="UTF-8"?>
<flow xmlns="http://www.springframework.org/schema/webflow"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/webflow
http://www.springframework.org/schema/webflow/spring-webflow-2.0.xsd">
<persistence-context />
</flow>
그 다음으로 이 패턴을 적용할 Flow에 적절한 FlowExecutionListener를 설정하자. 하이버네이트를 사용한다면, HibernateFlowExecutionListener를 등록하고, JPA를 사용한다면, JpaFlowExecutionListener를 등록하자.
<webflow:flow-executor id="flowExecutor"
flow-registry="flowRegistry">
<webflow:flow-execution-listeners>
<webflow:listener ref="jpaFlowExecutionListener" />
</webflow:flow-execution-listeners>
</webflow:flow-executor>
<bean id="jpaFlowExecutionListener" class="org.springframework.webflow.persistence.JpaFlowExecutionListener">
<constructor-arg ref="entityManagerFactory" />
<constructor-arg ref="transactionManager" />
</bean>
Flow가 종료되는 시점에 커밋이 일어나게 하려면, end-state의 commit 속성을 입력하자.
<end-state id="bookingConfirmed" commit="true" />
이걸로 끝이다.
이제 Flow가 시작할 때 리스너가 flowScope에 새로운 EntityManager를 할당해서 제어하게 된다. Flow 내에서 스프링 기반 데이터 접근 객체를 사용해서 발생하는 데이터 접근 시에는 항상 이 EntityManager를 자동으로 사용하게 된다. 이러한 데이터 접근 연산은 중간 수정 내용의 고립성 유지를 위해 항상 트랜잭션 처리 대상이 되지 않고, 읽기 전용 트랜잭션에서만 실행되어야 한다.
Flow란 상이한 상황(context)에서 실행될 수 있는 재사용이 가능한 여러 단계들의 흐름을 캡슐화한 것을 의미한다. 모든 Flow는 아래와 같은 Root로 시작한다.
<?xml version="1.0" encoding="UTF-8"?>
<flow xmlns="http://www.springframework.org/schema/webflow"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/webflow
http://www.springframework.org/schema/webflow/spring-webflow-2.0.xsd">
</flow>
SWF에서 Flow는 “Sate(state)“로 부르는 일련의 단계들로 구성된다. Flow로 진입하게 되는 Sate는 일반적으로 사용자에게 보여지는 뷰가 된다. 이 뷰에서는 Sate를 제어하게 되는 이벤트가 발생한다. 이들 이벤트는 결과적으로 다른 뷰로 이동하게 되는 Transition(transition)을 일으키게 된다. 모든 state는 <flow/> 안에 정의하게 된다. 맨 처음 정의되는 state가 Flow의 시작점이 된다.
Flow는 웹 애플리케이션 개발자가 XML 기반 Flow 정의 언어를 사용해서 작성된다.
<?xml version="1.0" encoding="UTF-8"?>
<flow xmlns="http://www.springframework.org/schema/webflow"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/webflow
http://www.springframework.org/schema/webflow/spring-webflow-2.0.xsd">
<view-state id="enterBookingDetails" />
<view-state id="enterBookingDetails">
<transition on="submit" to="reviewBooking" />
</view-state>
<end-state id="bookingCancelled" />
</flow>
대부분의 Flow는 화면 이동 로직 뿐만 아니라, 애플리케이션의 비즈니스 서비스나 다른 행동을 호출할 필요가 있을 수 있다. Flow 내에서 Action을 취할 수 있는 여러 지점이 존재한다.
SWF에서 Action은 기본적으로 Unified EL이라는 간결한 표현 언어를 사용해서 정의하게 된다.
대부분 evaluate 구성 요소를 사용하게 된다. 이를 통해 Spring Bean에 있는 메소드나 다른 Flow 변수를 호출할 수 있다. 예를 들면 아래와 같다.
<!-- [1] entityManager Bean 의 persist 메소드에 booking 객체를 넣어 호출한다. -->
<evaluate expression="entityManager.persist(booking)" />
<!-- [2] findHotels 메소드 호출하고 실행결과 Hotels 객체를 flowScope 데이타 모델에 저장한다. -->
<evaluate expression="bookingService.findHotels(searchCriteria)" result="flowScope.hotels" />
<!-- [3] findHotels 메소드 호출하고 실행결과 Hotels 객체를 flowScope 데이타 모델에 저장시 dataModel 타입으로 변환하여 저장한다. -->
<evaluate expression="bookingService.findHotels(searchCriteria)" result="flowScope.hotels" result-type="dataModel"/>
아래 예에서는 Flow가 시작할 때 Flow 범위에 Booking 객체를 생성해 저장한다. hotelId는 Flow의 입력 속성으로 받게 된다.
<flow xmlns="http://www.springframework.org/schema/webflow"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/webflow
http://www.springframework.org/schema/webflow/spring-webflow-2.0.xsd">
<input name="hotelId" />
<on-start>
<evaluate expression="bookingService.createBooking(hotelId, currentUser.name)" result="flowScope.booking" />
</on-start>
<view-state id="enterBookingDetails">
<transition on="submit" to="reviewBooking" />
</view-state>
<view-state id="reviewBooking">
<transition on="confirm" to="bookingConfirmed" />
<transition on="revise" to="enterBookingDetails" />
<transition on="cancel" to="bookingCancelled" />
</view-state>
<end-state id="bookingConfirmed" />
<end-state id="bookingCancelled" />
</flow>
각각의 Flow는 잘 정의된 입력/출력 계약(input/output contract)를 갖고 있다. Flow는 시작할 때 입력 속성을 건네받게 되고, 종료될 때 출력 속성을 반환하게 된다. 이처럼 Flow 호출은 개념적으로 다음과 같은 메소드 호출과 비슷하다.
FlowOutcome flowId(Map<String, Object> inputAttributes);
반환되는 FlowOutcome은 다음과 같은 메소드 선언부를 갖게 된다.
public interface FlowOutcome {
public String getName();
public Map<String, Object> getOutputAttributes();
}
<!-- [1] 해당 변수의 값은 flow scope 내에 hotelId 이란 이름으로 저장된다. -->
<input name="hotelId" />
<!-- [2] type 속성으로 속성 지정 가능. 타입이 일치하지 않다면 타입 변환 시도 -->
<input name="hotelId" type="long" />
<!-- [3] value 속성으로 입력 값을 할당 -->
<input name="hotelId" value="flowScope.myParameterObject.hotelId" />
<!-- [4] required 속성으로 null이나 비어있지 못하도록 강제 -->
<input name="hotelId" type="long" value="flowScope.hotelId" required="true" />
Flow 출력 속성은 output 구성 요소를 사용한다. output 속성은 end-state 내에 선언한다. 출력 값은 속성의 이름으로 Flow 범위 내에서 얻어오게 된다.
<end-state id="bookingConfirmed">
<output name="bookingId" />
</end-state>
<!-- 직접 대상 값 지정 -->
<end-state id="bookingConfirmed">
<output name="confirmationNumber" value="booking.confirmationNumber" />
</end-state>
<flow xmlns="http://www.springframework.org/schema/webflow"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/webflow
http://www.springframework.org/schema/webflow/spring-webflow-2.0.xsd">
<input name="hotelId" />
<on-start>
<evaluate expression="bookingService.createBooking(hotelId, currentUser.name)" result="flowScope.booking" />
</on-start>
<view-state id="enterBookingDetails">
<transition on="submit" to="reviewBooking" />
</view-state>
<view-state id="reviewBooking">
<transition on="confirm" to="bookingConfirmed" />
<transition on="revise" to="enterBookingDetails" />
<transition on="cancel" to="bookingCancelled" />
</view-state>
<end-state id="bookingConfirmed">
<output name="bookingId" value="booking.id" />
</end-state>
<end-state id="bookingCancelled" />
</flow>
위 Flow는 이제 hotelId를 입력 값으로 받아서, 새로운 예약이 끝나게 되면 bookingId 출력 속성을 결과로 반환하게 된다.
Flow에는 하나 이상의 인스턴스 변수 선언이 가능하다. 이 변수들은 flow가 시작할 때 할당되며, 변수를 유지하게 되는 모든 @Autowired transient 참조는 Flow가 재시작될 때 다시 값이 할당(rewired) 되게 된다. var 구성 요소를 사용해서 Flow 변수를 선언하자.
<var name="searchCriteria" class="com.mycompany.myapp.hotels.search.SearchCriteria"/>
변수로 사용하는 클래스가 Flow 요청 간 인스턴스의 Sate를 유지하기 위해서 java.io.Serializable을 interface로 가지고 있어야 함을 기억하자.
Flow 내에서 하위 Flow로써 또 다른 Flow 호출이 가능하다. 이때 하위 Flow가 결과를 반환할 때까지 기존 Flow는 대기하게 된다.
subflow-state 구성요소를 사용해서 하위 Flow 호출을 하게 된다.
<subflow-state id="addGuest" subflow="createGuest">
<transition on="guestCreated" to="reviewBooking">
<evaluate expression="booking.guests.add(currentEvent.attributes.guest)" />
</transition>
<transition on="creationCancelled" to="reviewBooking" />
</subfow-state>
이 예제에서는 createGuest Flow를 호출한다. guestCreated 출력이 반환되면, 새로운 손님이 예약 손님 리스트에 추가된다.
input 구성요소를 사용하면 하위 Flow에 입력값을 건낼 수 있다.
<subflow-state id="addGuest" subflow="createGuest">
<input name="booking" />
<transition to="reviewBooking" />
</subfow-state>
출력 값의 이름으로 하위 Flow에서 출력하는 속성을 참조해서 Transition을 하게 된다.
<subflow-state ..>
<transition on="guestCreated" to="reviewBooking">
<evaluate expression="booking.guests.add(currentEvent.attributes.guest)" />
</transition>
..
이 예에서는 guestCreated을 반환하게 될 때 gest 이름으로 넘어온 값을 booking 내의 guests (currentEvent.attributes.guest)의 일부로 추가해주고 있다.
아래는 샘플 코드이다.
<flow xmlns="http://www.springframework.org/schema/webflow"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/webflow
http://www.springframework.org/schema/webflow/spring-webflow-2.0.xsd">
<input name="hotelId" />
<on-start>
<evaluate expression="bookingService.createBooking(hotelId, currentUser.name)"
result="flowScope.booking" />
</on-start>
<view-state id="enterBookingDetails">
<transition on="submit" to="reviewBooking" />
</view-state>
<view-state id="reviewBooking">
<transition on="addGuest" to="addGuest" />
<transition on="confirm" to="bookingConfirmed" />
<transition on="revise" to="enterBookingDetails" />
<transition on="cancel" to="bookingCancelled" />
</view-state>
<subflow-state id="addGuest" subflow="createGuest">
<transition on="guestCreated" to="reviewBooking">
<evaluate expression="booking.guests.add(currentEvent.attributes.guest)" />
</transition>
<transition on="creationCancelled" to="reviewBooking" />
</subfow-state>
<end-state id="bookingConfirmed">
<output name="bookingId" value="booking.id" />
</end-state>
<end-state id="bookingCancelled" />
</flow>
Web Flow는 데이터 모델 및 action 실행을 위해 EL을 이용한다. 우리는 EL에 대해 알아보면서 flow 정의를 해보도록 하겠다.
기본으로는 Unified EL을 사용한다. jboss-el이 기본 구현체로 되어 있다.
참고 : web 컨테이너에서는 대게 el-api 를 지원해준다. 톰캣 6의 경우처럼 말이다.
OGNL은 SWF2에서 제공하는 또 다른 EL이다. 클래스패스에만 추가하면 자동으로 찾아서 사용한다.
Unified EL과 OGNL은 비슷한 문법을 가지고 있다. 가능하면 Unified EL만 사용하자.
Flow에서 EL 사용하는 경우
Flow에 의해서 보여지는 뷰는 EL을 사용해서 Flow 데이터 구조에 접근하게 됨
가장 일반적인 방법은 eval 표현으로, 이 경우 ${}나 #{}을 사용하면 안 된다. 이 예는 searchCriteria에 있는 nextPage() 호출.
<evaluate expression="searchCriteria.nextPage()" />
다음은 “template” 표현식으로 아래와 같은 형태로 ${} 을 사용할 수 있다.
<view-state id="error" view="error-${externalContext.locale}.xhtml" />
externalContext 에 세팅되어 있는 locale 결과를 대체하여 error-결과.xhtml 로 생성된
<evaluate expression="searchService.findHotel(hotelId)" result="flowScope.hotel" />
<on-render>
<evaluate expression="searchService.findHotels(searchCriteria)" result="viewScope.hotels" result-type="dataModel" />
</on-render>
<set name="requestScope.hotelId" value="requestParameters.id" type="long" />
<set name="flashScope.statusMessage" value="'Booking confirmed'" />
convesation 변수에 할당. 최상위 Flow가 시작할 때 할당되며, 최상위 Flow가 종료될 때 정리. 최상위 Flow의 자식 Flow에서 공유. HTTP session에 저장되며, 세션 복제를 할 경우를 대비해 Serizalizable를 구현해야 함.
<evaluate expression="searchService.findHotel(hotelId)" result="conversationScope.hotel"/>
<evaluate expression="bookingValidator.validate(booking, messageContext)" />
<evaluate expression="searchService.suggestHotels(externalContext.sessionMap.userProfile)" result="viewScope.hotels" />
<set name="requestScope.hotelId" value="requestParameters.id" type="long" />
<evaluate expression="booking.guests.add(currentEvent.guest)" />
<evaluate expression="bookingService.createBooking(hotelId, currentUser.name)" result="flowScope.booking" />
<set name="flashScope.successMessage" value="resourceBundle.successMessage" />
특정 범위에 변수를 할당할 때는 반드시 범위를 명시해야 한다.
<set name="requestScope.hotelId" value="requestParameters.id" type="long" />
특정 범위에 있는 변수에 접근할 때는 꼭 범위를 명시할 필요는 없다.
<evaluate expression="entityManager.persist(booking)" />
booking처럼 범위를 명시하지 않은 경우, 범위 검색 알고리즘(scope searching algorithm)이 동작하며, 이 알고리즘은 request→flash→view→flow→conversation 범위의 순서로 찾게 된다. 없을 경우 EvaluationException 발생. 아래그림은 검색되는 Scope 순서를 잘 보여주고 있다.

view-state는 flow 내에서 화면을 생성하는 요소이다. 여기서는 view-state 에 대해서 알아보도록 하자.
view-state는 기본적으로 해당 뷰를 생성하여 보여준 후, 사용자가 화면을 통해 응답을 하는 것을 기다린다. 아래는 view-state는 enterBookingDetails라는 ID 를 가지고 있으며 또한 별도의 view 설정이 없기 때문에 ID 가 곧 view를 뜻한다
<view-state id="enterBookingDetails">
<transition on="submit" to="reviewBooking" />
</view-state>
따라서. 디렉토리 상의

booking.xml(or booking-flow.xml) 이 존재하는 디렉토리에 있는 enterBookingDetails.jsp 이 자동으로 view로 동작한다. 또는 절대 경로를 이용하여 명시적으로 view=”/WEB-INF/hotels/booking/enterBookingDetails.jsp” 설정할 수도 있다. 아래에서 다시 설명하겠다.
뷰 속성을 여러 방법으로 지정할 수 있다.
<view-state id="enterBookingDetails" view="bookingDetails.xhtml">
<view-state id="enterBookingDetails" view="/WEB-INF/hotels/booking/bookingDetails.jsp>
<view-state id="enterBookingDetails" view="bookingDetails">
view-state 내부에서 유지되는 변수. Ajax 요청처럼 동일한 뷰를 여러 번 보여줘야 하는 경우 유용.
<var name="searchCriteria" class="com.mycompany.myapp.hotels.SearchCriteria" />
<on-render>
<evaluate expression="bookingService.findHotels(searchCriteria)" result="viewScope.hotels"/>
</on-render>
아래 코드는 화면 ID가 searchResults인 view-state 화면을 그리되 그리기 전 bookingService.findHotels(searchCriteria) 메소드를 호출한 후 그 결과를 viewScope내의 hotels로 저장한 후 화면을 보여주고 있다. 그리고 화면상에 next 또는 previous 이벤트 발생시 eval(searchCriteria.nextPage()/previousPage()) 이 발생 그 결과를 fragments으로 지정된 영역에 뿌려주고 있다.
<view-state id="searchResults">
<on-render>
<evaluate expression="bookingService.findHotels(searchCriteria)"
result="viewScope.hotels" />
</on-render>
<transition on="next">
<evaluate expression="searchCriteria.nextPage()" />
<render fragments="searchResultsFragment" />
</transition>
<transition on="previous">
<evaluate expression="searchCriteria.previousPage()" />
<render fragments="searchResultsFragment" />
</transition>
</view-state>
자세한 것은 아래에서 다시 설명하도록 하겠다.
뷰를 보여주기 전에 특정 액션을 실행하려면 on-render를 사용한다.
<on-render>
<evaluate expression="bookingService.findHotels(searchCriteria)" result="viewScope.hotels" />
</on-render>
<view-state id="enterBookingDetails" model="booking">
뷰 이벤트가 발생했을 때 지정된 모델에 대해서 다음 행동이 일어난다.
org.springframework.binding.convert.converters.TwoWayConverter을 구현하면 된다. StringToObject를 구현하는게 더 좋다.
protected abstract Object toObject(String string, Class targetClass) throws Exception;
protected abstract String toString(Object object) throws Exception;
구현 예.
public class StringToMonetaryAmount extends StringToObject {
public StringToMonetaryAmount() {
super(MonetaryAmount.class);
}
@Override
protected Object toObject(String string, Class targetClass) {
return MonetaryAmount.valueOf(string);
}
@Override
protected String toString(Object object) {
MonetaryAmount amount = (MonetaryAmount) object;
return amount.toString();
}
}
org.springframework.binding.convert.converters에 이미 구현된 변환기가 위치.
org.springframework.binding.convert.service.DefaultConversionService을 상속해서 addDefaultConverters() 메소드를 재정의 하면 된다. 자세한 것은 시스템 설정에서 ConversionService 확장을 이용하여 설정하는 곳에서 다루고 있다.
bind 속성으로 특정 뷰 이벤트에서 모델 바인딩과 유효성 검증을 안 하게 할 수도 있다.
<view-state id="enterBookingDetails" model="booking">
<transition on="proceed" to="reviewBooking">
<transition on="cancel" to="bookingCancelled" bind="false" />
</view-state>
아래와 같이 binder 속성으로 바인딩 할 프로퍼티를 명시적으로 지정할 수 있다.
<view-state id="enterBookingDetails" model="booking">
<binder>
<binding property="creditCard" />
<binding property="creditCardName" />
<binding property="creditCardExpiryMonth" />
<binding property="creditCardExpiryYear" />
</binder>
<transition on="proceed" to="reviewBooking" />
<transition on="cancel" to="cancel" bind="false" />
</view-state>
binder로 지정하지 않으면 모든 프로퍼티를 바인딩된다. converter를 이용하여 변환기 지정이 가능하다.
<view-state id="enterBookingDetails" model="booking">
<binder>
<binding property="checkinDate" converter="shortDate" />
<binding property="checkoutDate" converter="shortDate" />
<binding property="creditCard" />
<binding property="creditCardName" />
<binding property="creditCardExpiryMonth" />
<binding property="creditCardExpiryYear" />
</binder>
<transition on="proceed" to="reviewBooking" />
<transition on="cancel" to="cancel" bind="false" />
</view-state>
Model 유효성 검사에 대한 부분은 Web flow에서는 프로그래밍적으로 제약사항을 강제화하는 형태로 지원하고 있다.
첫 번째 방법으로 유효성 검증 로직을 모델 객체 내에 정의하는 방법이다. Web Flow 는 view-stat에서 모델로 넘어간 시점(view-state postback lifecycle)에서 자동적으로 validate 메소드를 자동으로 호출한다.
<view-state id="enterBookingDetails" model="booking">
<transition on="proceed" to="reviewBooking">
</view-state>
Booking class 내의 validate(view-state 명) 코드는 아래와 같이 볼 수 있다.(메소드명 : validate + EnterBookingDetails)
public class Booking {
private Date checkinDate;
private Date checkoutDate;
...
public void validateEnterBookingDetails(ValidationContext context) {
MessageContext messages = context.getMessages();
if (checkinDate.before(today())) {
messages.addMessage(new MessageBuilder().error().source("checkinDate").
defaultText("Check in date must be a future date").build());
} else if (!checkinDate.before(checkoutDate)) {
messages.addMessage(new MessageBuilder().error()
.source("checkoutDate")
.defaultText("Check out date must be later than check in date")
.build());
}
}
}
enterBookingDetails에 대한 이벤트가 발생했을 때 자동으로 validateEnterBookingDetails이 호출 된다.
메소드 이름을 validate$
Validator로 불리는 별도의 객체로 정의할 수도 있다. 클래스 이름을 $
@Component
public class BookingValidator {
public void validateEnterBookingDetails(Booking booking, ValidationContext context) {
MessageContext messages = context.getMessages();
if (booking.getCheckinDate().before(today())) {
messages.addMessage(new MessageBuilder().error()
.source("checkinDate")
.defaultText("Check in date must be a future date")
.build());
} else if (!booking.getCheckinDate().before(booking.getCheckoutDate())) {
messages.addMessage(new MessageBuilder().error()
.source("checkoutDate")
.defaultText("Check out date must be later than check in date")
.build());
}
}
}
spring mvc의 Error 객체도 받을 수 있다.
유효성 검증 동안에 MessageContext에 접근할 수 있게 해주며, 다양한 객체에 접근 가능하게 해준다.
validate=“false”로 설정하면 유효성 검사를 하지 않을 수 있다.
<view-state id="chooseAmenities" model="booking">
<transition on="proceed" to="reviewBooking">
<transition on="back" to="enterBookingDetails" validate="false" />
</view-state>
전이 대상은 (1)다른 뷰, (2)현재 뷰를 다시, (3)action을 실행, (4)Ajax 이벤트를 제어할 때 ‘fragments’로 불리는 일부 뷰를 보여주라는 요청일 수도 있다.
<transition on="submit" to="bookingConfirmed">
<evaluate expression="bookingAction.makeBooking(booking, messageContext)" />
</transition>
public class BookingAction {
public boolean makeBooking(Booking booking, MessageContext context) {
try {
bookingService.make(booking);
return true;
} catch (RoomNotAvailableException e) {
context.addMessage(builder.error().defaultText("No room is available at this hotel").build());
return false;
}
}
}
<global-transitions>
<transition on="login" to="login">
<transition on="logout" to="logout">
</global-transitions>
<transition on="event">
<!-- Handle event -->
</transition>
현재 뷰 중 일부만을 다시 보여줄 수 있는 방법으로, Ajax 기반일 때 주로 사용한다.
<transition on="next">
<evaluate expression="searchCriteria.nextPage()" />
<render fragments="searchResultsFragment" />
</transition>
‘,‘로 구분해서 다수의 fragment를 지정할 수도 있다.
MessageContext는 플로우 실행 동안에 메세지를 저장하는 데 사용되는 API다. 일반 메세지나 국제화가 지원된 메세지 모두 사용 가능하다. 메세지 수준도 지정 가능하며, 지원되는 수준은 info, warning, error이 있다. 메세지를 추가할 때는 MessageBuilder를 사용하자.
MessageContext context = ...
MessageBuilder builder = new MessageBuilder();
context.addMessage(builder.error().source("checkinDate").defaultText("Check in date must be a future date").build());
context.addMessage(builder.warn().source("smoking").defaultText("Smoking is bad for your health").build());
context.addMessage(builder.info().defaultText("We have processed your reservation - thank you and enjoy your stay").build());
MessageContext context = ...
MessageBuilder builder = new MessageBuilder();
context.addMessage(builder.error().source("checkinDate").code("checkinDate.notFuture").build());
context.addMessage(builder.warn().source("smoking").code("notHealthy").resolvableArg("smoking").build());
스프링의 MessageSource를 사용해서 메세지 번들 정의가 가능하다. 간단히 프로퍼티 파일로 관리하면 된다.
#messages.properties
checkinDate=Check in date must be a future date
notHealthy={0} is bad for your health
reservationConfirmation=We have processed your reservation - thank you and enjoy your stay
뷰나 플로우에서는 resourceBundle EL 변수로 접근도 가능하다.
<h:outputText value="#{resourceBundle.reservationConfirmation}" />
시스템에서 발생한 예외에 대해 메세지 지정할 수 있다. 예를 들어 타입 변환 시 예외가 발생하면 typeMismatch를 통해서 메세지 지정할 수 있다.
booking.checkinDate.typeMismatch=The check in date must be in the format yyyy-mm-dd.
모달 팝업 다이얼로그를 뷰로 렌더링하고 싶다면, view-state 내에 popup=“true”로 설정하면 된다.
<view-state id="changeSearchCriteria" view="enterSearchCriteria.xhtml" popup="true">
특히 스프링 자바 스크립트와 함께 사용하면, 팝업을 보여주는데 클라이언트 코드가 전혀 필요 없다. SWF가 클라이언트 요청을 팝업으로 재전송(redirect)해준다.
기본적으로 브라우저의 백 버튼으로 이전 view-state로 돌아갈 수 있다. history를 사용해서 이에 대한 설정이 가능하다.
<transition on="cancel" to="bookingCancelled" history="discard">
<transition on="confirm" to="bookingConfirmed" history="invalidate">
action-state는 flow 내에서 action 실행을 제어하기 위한 요소이다.
decision-state를 이용하여 if-else와 같은 흐름 제어를 할 수 있다. 좀 더 자세히 알아보도록 하자.
특정 액션을 호출한 다음에, 그 결과에 따라서 다른 상태로 전이하고 싶은 경우에는 action-state 구성 요소를 사용하자.
직관적으로 봤을 때 아래 코드는 interview.moreAnswersNeeded()의 결과값에 의해 transition이 실행될 것을 예상할 수 있다.
<action-state id="moreAnswersNeeded">
<evaluate expression="interview.moreAnswersNeeded()" />
<transition on="yes" to="answerQuestions" />
<transition on="no" to="finish" />
</action-state>
좀더 완전한 예를 살펴보자.
<flow xmlns="http://www.springframework.org/schema/webflow"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/webflow
http://www.springframework.org/schema/webflow/spring-webflow-2.0.xsd">
<on-start>
<evaluate expression="interviewFactory.createInterview()"
result="flowScope.interview" />
</on-start>
<view-state id="answerQuestions" model="questionSet">
<on-entry>
<evaluate expression="interview.getNextQuestionSet()"
result="viewScope.questionSet" />
</on-entry>
<transition on="submitAnswers" to="moreAnswersNeeded">
<evaluate expression="interview.recordAnswers(questionSet)" />
</transition>
</view-state>
<action-state id="moreAnswersNeeded">
<evaluate expression="interview.moreAnswersNeeded()" />
<transition on="yes" to="answerQuestions" />
<transition on="no" to="finish" />
</action-state>
<end-state id="finish" />
</flow>
action-state를 대신해서 편리하게 if/else 문법을 사용해서 이동하고자 하는 의사결정을 해주는 decision-state를 사용한다.
이전 예제를 의사결정 상태로 구현한 예를 보자.
<decision-state id="moreAnswersNeeded">
<if test="interview.moreAnswersNeeded()" then="answerQuestions" else="finish" />
</decision-state>
액션은 대부분 POJO의 메소드를 호출한다. action-state와 decision-state을 호출했을 때,
이들 메소드가 반환하는 값은 상태를 전이하게 해주는데 사용할 수 있다. 전이가 이벤트에 의해서 발생되기 때문에, 우선 메소드가 반환하는 값은 반드시 Event 객체에 매핑되어야 한다.
다음 테이블은 공통적으로 반환하는 값 타입에 따라 Event 객체가 어떻게 매핑되는지를 설명해준다.
메소드 반환 타입 매핑된 Event 식별자 표현
| 결과로 리턴되는 타입 | 메핑되는 이벤트 값 |
|---|---|
| java.lang.String | String 값 |
| java.lang.Boolean | yes(true에 해당), no(false에 해당) |
| java.lang.Enum Enum | Enum 이름 |
| 나머지 다른 타입 | success |
예제.moreAnswersNeeded() 메소드의 리턴 타입은 boolean인 것을 예상할 수 있으면 그에 따라 yes, no에 매핑됨을 알 수 있다.
<action-state id="moreAnswersNeeded">
<evaluate expression="interview.moreAnswersNeeded()" />
<transition on="yes" to="answerQuestions" />
<transition on="no" to="finish" />
</action-state>
POJO 로직처럼 action 코드를 작성하는 것이 가장 일반적이다.
때로는 flow context에 접근할 필요가 있는 액션 코드를 작성할 필요가 있다.
이럴 때는 POJO를 호출하면서, EL 변수로 flowRequestContext를 건낼 수 있다.
그 대신 Action 인터페이스를 구현하거나, MultiAction 기본 클래스를 상속할 수도 있다.
<evaluate expression="pojoAction.method(flowRequestContext)" />
public class PojoAction {
public String method(RequestContext context) {
...
}
}
<evaluate expression="customAction" />
public class CustomAction implements Action {
public Event execute(RequestContext context) {
...
}
}
<evaluate expression="multiAction.actionMethod1" />
public class CustomMultiAction extends MultiAction {
public Event actionMethod1(RequestContext context) {
...
}
...
public Event actionMethod2(RequestContext context) {
...
}
}
action은 복잡한 비즈니스 로직을 캡슐화하고 있는 서비스를 호출할 수도 있다.
이 서비스들은 비즈니스 예외를 던질 수도 있으니 이를 처리해야 할 수도 있다.
<evaluate expression="bookingAction.makeBooking(booking, flowRequestContext)" />
public class BookingAction {
public String makeBooking(Booking booking, RequestContext context) {
try {
BookingConfirmation confirmation = bookingService.make(booking);
context.getFlowScope().put("confirmation", confirmation);
return "success";
} catch (RoomNotAvailableException e) {
context.addMessage(new MessageBuilder().error().efaultText("No room is available at this hotel").build());
return "error";
}
}
}
아래 예제는 이전 예제와 기능적으로는 동일하지만, POJO 액션 대신 MultiAction으로 구현했다.
Event ${methodName}(RequestContext) 규약에 따라 메소드를 구성하면 되고, POJO의 자유스러움에 비해, 보다 더 강력한 타입 안정성을 제공한다.
<evaluate expression="bookingAction.makeBooking" />
public class BookingAction extends MultiAction {
public Event makeBooking(RequestContext context) {
try {
Booking booking = (Booking) context.getFlowScope().get("booking");
BookingConfirmation confirmation = bookingService.make(booking);
context.getFlowScope().put("confirmation", confirmation);
return success();
} catch (RoomNotAvailableException e) {
context.getMessageContext()
.addMessage(new MessageBuilder().error().defaultText("No room is available at this hotel").build());
return error();
}
}
}
<flow xmlns="http://www.springframework.org/schema/webflow"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/webflow
http://www.springframework.org/schema/webflow/spring-webflow-2.0.xsd">
<input name="hotelId" />
<on-start>
<evaluate expression="bookingService.createBooking(hotelId, currentUser.name)"
result="flowScope.booking" />
</on-start>
</flow>
<view-state id="changeSearchCriteria" view="enterSearchCriteria.xhtml"
popup="true">
<on-entry>
<render fragments="hotelSearchForm" />
</on-entry>
</view-state>
<view-state id="editOrder">
<on-entry>
<evaluate expression="orderService.selectForUpdate(orderId, currentUser)"
result="viewScope.order" />
</on-entry>
<transition on="save" to="finish">
<evaluate expression="orderService.update(order, currentUser)" />
</transition>
<on-exit>
<evaluate expression="orderService.releaseLock(order, currentUser)" />
</on-exit>
</view-state>
<flow xmlns="http://www.springframework.org/schema/webflow"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/webflow
http://www.springframework.org/schema/webflow/spring-webflow-2.0.xsd">
<input name="orderId" />
<on-start>
<evaluate expression="orderService.selectForUpdate(orderId, currentUser)"
result="flowScope.order" />
</on-start>
<view-state id="editOrder">
<transition on="save" to="finish">
<evaluate expression="orderService.update(order, currentUser)" />
</transition>
</view-state>
<on-end>
<evaluate expression="orderService.releaseLock(order, currentUser)" />
</on-end>
</flow>
<view-state id="reviewHotels">
<on-render>
<evaluate expression="bookingService.findHotels(searchCriteria)"
result="viewScope.hotels" result-type="dataModel" />
</on-render>
<transition on="select" to="reviewHotel">
<set name="flowScope.hotel" value="hotels.selectedRow" />
</transition>
</view-state>
<subflow-state id="addGuest" subflow="createGuest">
<transition on="guestCreated" to="reviewBooking">
<evaluate expression="booking.guestList.add(currentEvent.attributes.newGuest)" />
</transition>
</subfow-state>
<action-state id="doTwoThings">
<evaluate expression="service.thingOne()">
<attribute name="name" value="thingOne" />
</evaluate>
<evaluate expression="service.thingTwo()">
<attribute name="name" value="thingTwo" />
</evaluate>
<transition on="thingTwo.success" to="showResults" />
</action-state>
아래 예는 flow에서 printBoardingPassAction를 호출하는 것으로 PDF로 프린트하고자 할 때 구현하는 예를 보여주고 있다.
AbstractAction을 상속한 PrintBoardingPassAction의 doExecute() 메소드 안에 실제 pdf 관련 소스를 구현하고 success()를 리턴한다.
<view-state id="reviewItinerary">
<transition on="print">
<evaluate expression="printBoardingPassAction" />
</transition>
</view-state>
public class PrintBoardingPassAction extends AbstractAction {
public Event doExecute(RequestContext context) {
// stream PDF content here...
// - Access HttpServletResponse by calling context.getExternalContext().getNativeResponse();
// - Mark response complete by calling context.getExternalContext().recordResponseComplete();
return success();
}
}
Flow 상속은 한 Flow가 다른 Flow 설정을 상속할 수 있게 되어 있다. 상속은 Flow와 State 레벨에서 모두 발생할 수 있다. 가장 흔한 유즈케이스는 상위 Flow로 global transition과 예외 핸들러를 정의하고 하위 Flow로 그 설정을 상속받는 것이다. 상위 Flow를 찾으려면 다른 Flow들처럼 flow-registry에 추가해야 된다.
상위에 정의한 요소를 하위에서 접근할 수 있다는 측면에서는 자바 상속과 Flow 상속이 비슷하다. 하지만 몇 가지 차이점을 가지고 있다.
하위 Flow는 상위 Flow의 요소를 재정의할 수 없다. 상위와 하위 Flow에 있는 동일한 요소는 병합된다. 상위 Flow에만 있는 요소는 하위 Flow에 추가된다.
하위 Flow는 여러 상위 Flow를 상속받을 수 있다. 그러나, 자바 상속은 단일 클래스로 제한된다.
Flow 수준 상속은 flow 내에 parent 속성을 이용하여 정의한다. 이 속성은 콤마로 구분하여 상속받을 Flow를 표현한다. 하위 flow는 목록에 명시된 순서대로 각각의 상위 Flow를 상속받는다. 첫 번째 상속으로 상위 Flow에 있는 요소와 내용을 추가하고 나면 그것을 다시 하위 Flow로 간주하고 그 다음 상위 flow를 상속 받는다. 아래 예를 보면 common-transitions를 먼저 상속 받고 다음에 common-states를 상속 받는다.
<flow parent="common-transitions, common-states">
State 수준 상속은 Flow 수준 상속과 비슷하다. 유일한 차이점은 Flow 전체가 아니라 오직 해당 State 하나만 상위로부터 상속받는다. Flow 상속과 달리 오직 하나의 상위만 허용한다. 또한 상속받을 Flow State의 식별자가 반드시 정의되어 있어야 한다. Flow와 State 식별자는 #로 구분한다. 상위와 하위 State는 반드시 같은 타입이어야 한다. 예를 들어 view-state는 ent-state를 상속받을 수 없다. 오직 view-state만 상속받을 수 있다
<view-state id="child-state" parent="parent-flow#parent-view-state">
종종 상위 Flow는 직접 호출하지 않도록 설계한다. 그런 Flow를 실행하지 못하도록 abstract로 설정할 수 있다. 만약 추상 Flow를 실행하려고 하면 FlowBuilderException가 발생한다.
<flow abstract="true">
하위 Flow가 상위 Flow를 상속할 때 발생하는 기본적인 일은 상위와 하위 Flow를 병합하여 새로운 Flow를 만드는 것이다. 웹 Flow 정의 언어에는 각각의 엘리먼트에 대해 어떻게 병합할 것인가에 대한 규칙이 있다. 엘리먼트에는 두 종류가 있다. 병합 가능한 것(mergeable)과 병합이 가능하지 않은 것(non-mergeable)이 있다. 병합 가능(mergeable)한 엘리먼트는 만약 엘리먼트가 같다면 병합을 시도한다. 병합이 가능하지 않은(non-mergeable) 엘리먼트는 항상 최종 Flow에 직접 포함된다. 병합 과정 중에 수정하지 않는다.
만약 같은 타입의 엘리먼트고 입력한 속성이 같다면 상위 엘리먼트의 내용을 하위 엘리먼트로 병합한다. 병합 알고리즘은 계속해서 병합하는 상위와 하위의 서브 엘리먼트를 각각 병합한다. 그렇지 않으면 상위 Flow의 엘리먼트를 하위 Flow에 새로운 엘리먼트로 추가한다.
대부분의 경우 상위 flow의 엘리먼트가 하위 Flow 엘리먼트에 추가된다. 이 규칙에 예외로는 시작할 때 추가될 action 엘리먼트(evaluate, render, set)가 있다. 상위 action의 결과를 하위 action 결과로 사용하게 한다.
병합이 가능한 엘리먼트는 다음과 같다.
병합할 수 없는 엘리먼트는 다음과 같다
데이터처리 서비스는 데이터베이스에 대한 연결 및 영속성 처리, 선언적인 트랜잭션 관리를 지원한다.
데이터베이스에 대한 연결을 제공하는 서비스이다. 다양한 방식의 데이터베이스 연결을 제공하고,이에 대한 추상화계층을 제공함으로써, 업무 로직과 데이터베이스 연결 방식 간의 종속성을 배제한다.
Connection Provider별 Connection 객체를 얻기 위한 로직을 구현한 DataSource 구현체를 사용한다.
JDBC driver를 이용하여 Database Connection을 생성한다.
<bean id="dataSource"
class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="${driver}" />
<property name="url" value="${dburl}" />
<property name="username" value="${username}" />
<property name="password" value="${password}" />
</bean>
| PROPERTIES | 설 명 |
|---|---|
| driverClassName | JDBC driver class name설정 |
| url | Database에 접근하기 위한 JDBC URL |
| username | Database 접근하기 위한 사용자명 |
| password | Database 접근하기 위한 암호 |
@Resource(name = "dataSource")
DataSource dataSource;
@Resource(name = "jdbcProperties")
Properties jdbcProperties;
boolean isHsql = true;
@Test
public void testJdbcDataSource() throws Exception {
assertNotNull(dataSource);
assertEquals("org.springframework.jdbc.datasource.DriverManagerDataSource",
dataSource.getClass().getName());
Connection con = null;
Statement stmt = null;
ResultSet rs = null;
try {
con = dataSource.getConnection();
assertNotNull(con);
stmt = con.createStatement();
rs = stmt.executeQuery("select 'x' as x from dual");
while (rs.next()) {
assertEquals("x", rs.getString(1));
}
........
JDBC driver를 이용한 Database Connection 구현체이다.Commons DBCP라 불리는 Jakarta의 Database Connection Pool이다.
<bean id="dataSource" class="org.apache.commons.dbcp2.BasicDataSource" destroy-method="close">
<property name="driverClassName" value="${driver}"/>
<property name="url" value="${dburl}"/>
<property name="username" value="${username}"/>
<property name="password" value="${password}"/>
<property name="defaultAutoCommit" value="false"/>
<property name="poolPreparedStatements" value="true"/>
</bean>
| PROPERTIES | 설 명 |
|---|---|
| driverClassName | jdbc driver의 class name 설정 |
| url | Database url을 설정 |
| username | Database 접근하기 위한 사용자명 |
| password | Database 접근하기 위한 암호 |
| defaultAutoCommit | datasource로부터 리턴된 connection에 대한 auto-commit 여부를 설정 |
| poolPreparedStatements | PreparedStatement 사용여부 |
| initialSize | Connection pool에 생성될 초기 connection size 설정 |
| maxTotal (1.x에서는 maxActive) | 동시에 할당할 수 있는 active connection 최대 갯수를 설정 |
| maxIdle | pool에 남겨놓을 수 있는 idle connection 최대 갯수를 설정 |
| minIdle | 최소한으로 유지할 connection 갯수를 설정 |
| maxWaitMillis (1.x에서는 maxWait) | 모든 Connection이 사용중일 경우 최대 대기 시간을 설정 |
| defaultReadOnly | Connection Pool에 의해 생성된 Connection에 read-only 속성 부여 |
| defaultTransactionIsolation | 리턴된 connection에 대한 transaction isolation 속성 부여 |
| defaultCatalog | Connection의 catlog 설정 |
| testOnBorrow | Connection pool에서 객체를 가지고 오기 전에 그 객체의 유효성을 확인할 것인지 결정 |
| testOnReturn | 객체를 return하기 전에 객체의 유효성을 확인할 것인지 결정 |
| validationQuery | validationQuery를 설정 |
| loginTimeout | Database에 연결하기 위한 login timeout(in seconds)을 설정 |
| timeBetweenEvictionRunsMillis | 놀고 있는 connection을 pool에서 제거하는 시간기준(기본 -1) 단위 1/1000초 |
@Resource(name = "dataSource")
DataSource dataSource;
@Resource(name = "jdbcProperties")
Properties jdbcProperties;
boolean isHsql = true;
@Test
public void testDbcpDataSource() throws Exception
{
assertNotNull(dataSource);
assertEquals("org.apache.commons.dbcp.BasicDataSource", dataSource.getClass().getName());
Connection con = null;
Statement stmt = null;
ResultSet rs = null;
try
{
con = dataSource.getConnection();
assertNotNull(con);
stmt = con.createStatement();
rs = stmt.executeQuery("select 'x' as x from dual");
while (rs.next()) {
assertEquals("x", rs.getString(1));
}
} catch (Exception e) {
fail("Jdbc DataSource Test Failed! : " + e.getMessage());
e.printStackTrace();
}
........
}
JDBC driver를 이용한 Database Connection를 생성하는 구현체. C3P0 Library에 관련 사항은 C3P0 Configuration에서 확인할 수 있다.
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource"
destroy-method="close">
<property name="driverClass" value="${driver}" />
<property name="jdbcUrl" value="${dburl}" />
<property name="user" value="${username}" />
<property name="password" value="${password}" />
<property name="initialPoolSize" value="3" />
<property name="minPoolSize" value="3" />
<property name="maxPoolSize" value="50" />
<!-- <property name="timeout" value="0" /> --> <!-- 0 means: no timeout -->
<property name="idleConnectionTestPeriod" value="200" />
<property name="acquireIncrement" value="1" />
<property name="maxStatements" value="0" /> <!-- 0 means: statement caching is turned off. -->
<!-- c3p0 is very asynchronous. Slow JDBC operations are generally performed
by helper threads that don't hold contended locks.
Spreading these operations over multiple threads can significantly improve performance
by allowing multiple operations to be performed simultaneously -->
<property name="numHelperThreads" value="3" /> <!-- 3 is default -->
</bean>
| PROPERTIES | 설 명 |
|---|---|
| driverClass | jdbc driver |
| jdbcUrl | DB URL |
| user | 사용자명 |
| password | 패스워드 |
| initialPoolSize | 풀 초기값 |
| minPoolSize | 풀 최소값 |
| maxPoolSize | 풀 최대값 |
| idleConnectionTestPeriod | idle상태 점검시간 |
| acquireIncrement | 증가값 |
| maxStatements | 캐쉬유지여부 |
| numHelperThreads | HelperThread 개수 |
@Resource(name = "dataSource")
DataSource dataSource;
@Resource(name = "jdbcProperties")
Properties jdbcProperties;
@Test
public void testC3p0DataSource() throws Exception
{
assertNotNull(dataSource);
assertEquals("com.mchange.v2.c3p0.ComboPooledDataSource", dataSource.getClass().getName());
Connection con = null;
Statement stmt = null;
ResultSet rs = null;
try {
con = dataSource.getConnection();
assertNotNull(con);
stmt = con.createStatement();
rs = stmt.executeQuery("select 'x' as x from dual");
while (rs.next()) {
assertEquals("x", rs.getString(1));
}
} catch (Exception e) {
fail("Jdbc DataSource Test Failed! : " + e.getMessage());
e.printStackTrace();
}
...................
}
JNDIDataSource는 JNDI Lookup을 이용하여 Database Connection을 생성한다. JNDIDataSource는 대부분 Enterprise application server에서 제공되는 JNDI tree로 부터 DataSource를 가져온다.
jee tag를 사용하기 위해서는 Spring XML Configuration 파일의 머릿말에 namespace와 schemaLocation를 추가해야 한다.
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:jee="http://www.springframework.org/schema/jee"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
http://www.springframework.org/schema/jee http://www.springframework.org/schema/jee/spring-jee-2.5.xsd">
<!-- <bean/> definitions here -->
</beans>
Jeus 설정
<jee:jndi-lookup id="dataSource" jndi-name="${jndiName}" resource-ref="true">
<jee:environment>
java.naming.factory.initial=${jeus.java.naming.factory.initial}
java.naming.provider.url=${jeus.java.naming.provider.url}
</jee:environment>
</jee:jndi-lookup>
Weblogic 설정
<util:properties id="jndiProperties" location="classpath:/META-INF/spring/jndi.properties" />
<jee:jndi-lookup id="dataSource" jndi-name="${jndiName}" resource-ref="true" environment-ref="jndiProperties" />
| PROPERTIES | 설 명 |
|---|---|
| jndiTemplate | JNDI 검색을 위해 JNDI 템플릿을 설정 |
| jndiEnvironment | JNDI를 검색하기 위해 JNDI 환경을 설정 |
| resourceRef | J2EE 컨테이너에서 검색할 수 있는지 설정 |
| expectedType | JNDI 객체의 타입을 지정 |
| jndiName | 검색을 위해 JNDI 이름을 설정 |
| proxyInterface | JNDI 객체를 사용하기 위해 proxy 인터페이스를 설정 |
| lookupOnStartup | starup시에 JNDI object를 검색할 지 여부를 설정 |
| cache | JNDI 객체를 캐싱할 것인지 설정 |
| defaultObject | JNDI lookup에 실패하였을 경우 전달할 default object를 지정 |
@Resource(name = "dataSource")
DataSource dataSource;
@Resource(name = "jdbcProperties")
Properties jdbcProperties;
@Test
public void testJndiJeusDataSource() throws Exception
{
assertNotNull(dataSource);
assertEquals("jeus.jdbc.connectionpool.DataSourceWrapper", dataSource.getClass().getName());
Connection con = null;
Statement stmt = null;
ResultSet rs = null;
try {
con = dataSource.getConnection();
assertNotNull(con);
stmt = con.createStatement();
rs = stmt.executeQuery("select 'x' as x from dual");
while (rs.next()) {
assertEquals("x", rs.getString(1));
}
} catch (Exception e) {
fail("Jdbc DataSource Test Failed! : " + e.getMessage());
e.printStackTrace();
}
...................
}
@Resource(name = "dataSource")
DataSource dataSource;
@Resource(name = "jdbcProperties")
Properties jdbcProperties;
@Test
public void testJndiDataSource() throws Exception
{
assertNotNull(dataSource);
assertEquals("weblogic.jdbc.common.internal.RmiDataSource_922_WLStub", dataSource.getClass().getName());
Connection con = null;
Statement stmt = null;
ResultSet rs = null;
try {
con = dataSource.getConnection();
assertNotNull(con);
stmt = con.createStatement();
rs = stmt.executeQuery("select 'x' as x from dual");
while (rs.next()) {
assertEquals("x", rs.getString(1));
}
} catch (Exception e) {
fail("Jdbc DataSource Test Failed! : " + e.getMessage());
e.printStackTrace();
}
...................
}
Data Access 서비스는 다양한 데이터베이스 솔루션 및 데이터베이스 접근 기술에 일관된 방식으로 대응하기 위한 서비스로서,데이터를 조회하거나 입력, 수정, 삭제하는 기능을 수행하는 메커니즘을 단순화한다. 또한 데이터베이스 솔루션이나 접근 기술이 변경될 경우에도 데이터를 다루는 시스템 영역의 변경을 최소화할 수 있도록 데이터베이스와의 접점을 추상화하며, 추상화된 데이터 접근 방식을 템플릿(Template)으로 제공함으로써, 개발자들의 업무 효율을 향상시킨다.
전자정부 프레임워크에서는 JDBC 를 사용한 Data Access 를 추상화하여 간편하고 쉽게 사용할 수 있는 Data Mapper framework 인 iBATIS 를 Data Access 기능의 기반 오픈 소스로 채택하였다. iBATIS 를 사용하면 관계형 데이터베이스에 억세스하기 위해 필요한 일련의 자바 코드 사용을 현저히 줄일 수 있으며 간단한 XML 기술을 사용하여 SQL 문을 JavaBeans (또는 Map) 에 간편하게 맵핑할 수 있다.
iBATIS Data Mapper API 는 XML을 사용하여 SQL 문에 대한 객체 맵핑을 간편하게 기술할 수 있도록 지원하며, 자바빈즈 객체와 Map 구현체, 다양한 원시 래퍼 타입(String, Integer..) 등을 PreparedStatement 의 파라메터나 ResultSet에 대한 결과 객체로 쉽게 맵핑해준다.

Data Access 서비스에 대한 자세한 설명에 앞서 간단하게 Data Access 서비스를 시작하는데 필요한 것에 대한 설명을 하고자 한다.
본 서비스를 활용하기 위해서 필요한 Library 목록과 설명은 아래와 같다.
| 라이브러리 | 설 명 | 연관 라이브러리 |
|---|---|---|
| ibatis-sqlmap-2.3.4.726.jar | iBATIS 라이브러리(필수) | |
| commons-dbcp-1.2.2.jar | database connection pooling 지원 라이브러리(선택) | |
| commons-logging-1.1.1.jar | commons 로깅(선택) | |
| log4j-1.3alpha-8.jar | log4j(선택) | |
| oscache-2.4.jar | 중앙집중 또는 분산 캐슁 지원(선택) | |
| cglib-nodep-2.1_3.jar | Runtime Bytecode Enhancing 필요 시(선택) |
ibatis-sqlmap-2.3.4.726.jar 만이 필수 라이브러리이다. 그러나 일반적으로 commons-dbcp 와 같은 커넥션풀링 라이브러리 및 로깅 처리를 위한 라이브러리는 반드시 필요로 하며, 추가적으로 iBATIS 에서 지원하는 개선된 기능으로 cache 지원 이나 Runtime Bytecode Enhancement 관련 기능을 쓰고자 할 경우는 위의 참조 라이브러리를 추가로 설정할 수 있다. 또한 우리는 Spring-iBATIS 연동 형태의 어플리케이션 개발을 선호하므로 Spring 관련 라이브러리 및 이에 대한 dependency 라이브러리가 일반적으로 함께 포함될 것이다. 여기에 덧붙여 실제 Data Access 처리의 대상의 되는 DBMS(Oracle, Mysql, Hsqldb, Tibero ..) 에 따라 적절한 jdbc 드라이버에 대한 라이브러리가 추가적으로 필요하다.
Spring 프레임워크 기반 어플리케이션에서 iBATIS 를 연동하여 사용하고자 하는 경우 Spring의 SqlMapClientFactoryBean 에 대한 설정이 필요하며 여기서는 실제 대상 sql-map-config 설정 파일과 iBATIS 에 제공될 dataSource 에 대한 설정을 지시한다.
<!-- SqlMap setup for iBATIS Database Layer -->
<bean id="sqlMapClient" class="egovframework.rte.psl.orm.ibatis.SqlMapClientFactoryBean">
<property name="configLocation" value="classpath:/META-INF/sqlmap/sql-map-config.xml"/>
<property name="dataSource" ref="dataSource"/>
</bean>
아래는 주된 iBATIS 의 SQL Map XML Configuration 파일(sql-map-config.xml 설정 파일)이다. iBATIS 단독으로 쓰일 때는 transactionManager, dataSource 설정 등을 추가로 포함해야 하지만 Spring 연동 환경에서는 이 부분은 Spring 이 넘겨주는 dataSource 를 자동으로 사용하게 되고 transaction 관리는 비즈니스 서비스 영역에 선언적으로 설정하여 iBATIS 관련 모듈에서는 고민할 필요가 없게 된다.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE sqlMapConfig PUBLIC "-//ibatis.apache.org//DTD SQL Map Config 2.0//EN"
"http://ibatis.apache.org/dtd/sql-map-config-2.dtd">
<sqlMapConfig>
<settings useStatementNamespaces="false"
..
/>
<typeHandler javaType="java.util.Calendar" jdbcType="TIMESTAMP"
callback="egovframework.rte.psl.dataaccess.typehandler.CalendarTypeHandler" />
<sqlMap resource="META-INF/sqlmap/mappings/testcase-basic.xml" />
<sqlMap ../>
..
</sqlMapConfig>
Spring 2.5.5 이상, iBATIS 2.3.2 이상 인 경우에는 iBATIS 연동을 위한 SqlMapClientFactoryBean 정의 시 mappingLocations 속성으로 Sql 매핑 파일에 대한 패턴 표현식으로 일괄 지정도 가능하다. mappingLocations 속성 사용 예는 다음과 같다.
<!-- SqlMap setup for iBATIS Database Layer -->
<bean id="sqlMapClient" class="egovframework.rte.psl.orm.ibatis.SqlMapClientFactoryBean">
<property name="configLocation" value="classpath:/META-INF/sqlmap/sql-map-config.xml"/>
<!-- Java 1.5 or higher and iBATIS 2.3.2 or higher REQUIRED -->
<property name="mappingLocations" value="classpath:/META-INF/sqlmap/mappings/**/*.xml" />
<property name="dataSource" ref="dataSource"/>
</bean>
이 경우는 “configLocation” 속성이 필요하지 않지만, 현재 해당 속성이 없는 경우 SqlMapClientFactoryBean의 초기화되지 않기 때문에 “configLocation” 속성을 유지하셔야 한다. 이 때 해당 sql-map-config.xml은 다음과 같이 dummy.xml query를 갖도록 처리하여야 한다.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE sqlMapConfig PUBLIC "-//ibatis.apache.org//DTD SQL Map Config 2.0//EN"
"http://ibatis.apache.org/dtd/sql-map-config-2.dtd">
<sqlMapConfig>
<sqlMap resource="sqlmap/sql/common/dummy.xml"/>
</sqlMapConfig>
dummy.xml은 다음과 같이 처리한다.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE sqlMap PUBLIC "-//iBATIS.com//DTD SQL Map 2.0//EN" "http://www.ibatis.com/dtd/sql-map-2.dtd">
<sqlMap namespace="Dummy">
</sqlMap>
iBATIS 에서 정의한 SQL Map 문서 구조 내에서 다양한 옵션 설정과 Mapped statement 정의를 작성하게 된다. 아래는 부서정보에 대한 CRUD 와 관련한 쿼리와 이에 대한 In/Out 객체 맵핑을 포함하는 간략한 매핑 파일이다.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE sqlMap PUBLIC "-//iBATIS.com//DTD SQL Map 2.0//EN" "http://www.ibatis.com/dtd/sql-map-2.dtd">
<sqlMap namespace="Dept">
<typeAlias alias="deptVO" type="egovframework.DeptVO" />
<resultMap id="deptResult" class="deptVO">
<result property="deptNo" column="DEPT_NO" />
<result property="deptName" column="DEPT_NAME" />
<result property="loc" column="LOC" />
</resultMap>
<insert id="insertDept" parameterClass="deptVO">
insert into DEPT
(DEPT_NO,
DEPT_NAME,
LOC)
values (#deptNo#,
#deptName#,
#loc#)
</insert>
<select id="selectDept" parameterClass="deptVO" resultMap="deptResult">
<![CDATA[
select DEPT_NO,
DEPT_NAME,
LOC
from DEPT
where DEPT_NO = #deptNo#
]]>
</select>
<update id="updateDept" parameterClass="deptVO">
update DEPT
set DEPT_NAME = #deptName#,
LOC = #loc#
where DEPT_NO = #deptNo#
</update>
<delete id="deleteDept" parameterClass="deptVO">
delete from DEPT
where DEPT_NO = #deptNo#
</delete>
<select id="selectDeptList" parameterClass="deptVO" resultMap="deptResult">
<![CDATA[
select DEPT_NO,
DEPT_NAME,
LOC
from DEPT
where 1 = 1
]]>
<isNotNull prepend="and" property="deptNo">
DEPT_NO = #deptNo#
</isNotNull>
<isNotNull prepend="and" property="deptName">
DEPT_NAME LIKE '%' || #deptName# || '%'
</isNotNull>
</select>
</sqlMap>
위의 CRUD 관련 매핑 파일에서는 기본으로 DeptVO 라는 JavaBeans 객체를 Parameter/Result 객체로 사용하고 있으며 이를 typeAlias 로 간략하게 지정하여 사용하고 있다. 위에서는 주로 parameterClass 로 지정하여 파라메터 객체에 대한 명시적 사용을 지시하고 있으며, 실제 바인드 변수 처리 시에는 #attribute명# 과 같이 Inline Parameter 형식을 사용하였다. 또한 resultMap 정의를 통하여 ResultSet 에 따른 결과 칼럼정보에 대한 결과 객체(DeptVO)의 필드별 매핑을 별도로 정의하였고 이를 select 문의 resultMap 속성에 명시하여 select 의 결과를 resultMap 을 통해 처리하고 있다. 이러한 방식 외에 다양한 방식으로 Mapped Statement 처리를 정의할 수 있으나, 위와 같이 JavaBeans 객체를 사용하고 또 resultMap 을 정의하여 결과객체 처리를 하며 Inline Parameter 방식으로 바인드 변수 처리하는 스타일로 사용하기를 권고하는 바이다.
간단한 형태의 DAO 클래스를 생성한다. 아래의 DAO 에서 상속하고 있는 EgovAbstractDAO 에서는 SqlMapClientDaoSupport 를 extends 하고 있으며 iBATIS SQL Map 상호 작용을 위한 기본 클래스인 SqlMapClient (위에서 “sqlMapClient” 로 정의한 iBATIS 연동 FactoryBean 에 의해 제공됨) 에 대한 injection 처리가 내부적으로 적용되어 있고, 기본 CRUD 에 대한 iBATIS 실행을 위해서는 간략한 메서드 래핑도 제공한다. 상세 API 를 사용하고자 하는 경우 getSqlMapClientTemplate() 를 통해(ex. getSqlMapClientTemplate().queryWithRowHandler(“selectEmpListToOutFileUsingRowHandler”, paramObject, rowHandler); ) 사용할 수 있다.
..
@Repository("deptDAO")
public class DeptDAO extends EgovAbstractDAO {
public void insertDept(DeptVO vo) {
insert("insertDept", vo);
}
public int updateDept(DeptVO vo) {
return update("updateDept", vo);
}
public int deleteDept(DeptVO vo) {
return delete("deleteDept", vo);
}
public DeptVO selectDept(DeptVO vo) {
return (DeptVO)selectByPk("selectDept", vo);
}
@SuppressWarnings("unchecked")
public List<DeptVO> selectDeptList(DeptVO searchVO) {
return list("selectDeptList", searchVO);
}
}
각 CRUD 에 관련한 메서드에서는 queryId 와 파라메터 객체(여기서는 DeptVO) 를 인자로 iBATIS 의 Mapped Statement 을 실행하고 있으며, 조회의 경우에는 단건 조회는 DeptVO 객체로, 리스트 조회는 DeptVO 에 대한 List 를 돌려주도록 하고 있다. iBATIS 내부적으로는 java 1.5 이상의 Generics (type 이 정의된 Collection 처리) 로 처리하지 않으나 sql 매핑 파일에 따라 실제 데이터는 List<DeptVO> 로 처리가 될 것이므로 @SuppressWarnings(“unchecked”) 을 지시하여 호출 이전 모듈에서는 불필요한 Type Casting 을 최소화하고 있다. 만약 sql-map-config.xml 의 settings 옵션으로 useStatementNamespaces=“true” 를 설정한 경우라면 위의 queryId 는 “Dept.insertDept” 와 같이 sql 맵핑 파일에 지정한 Namespace prefix 을 포함하는 형태여야 함에 유의한다.
위에서 정의한 설정 파일 및 DAO 를 이용하여 간단한 입력,조회 처리에 대한 JUnit TestCase 형태(JUnit 4 스타일)로 구성하였다.
..
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {"classpath*:META-INF/spring/context-*.xml" })
@TransactionConfiguration(transactionManager = "txManager", defaultRollback = false)
@Transactional
public class BasicDataAccessTest {
@Resource(name = "dataSource")
DataSource dataSource;
@Resource(name = "deptDAO")
DeptDAO deptDAO;
@Before
public void onSetUp() throws Exception {
// 외부에 sql file 로부터 DB 초기화 (기존 테이블 삭제/생성)
SimpleJdbcTestUtils.executeSqlScript(
new SimpleJdbcTemplate(dataSource), new ClassPathResource(
"META-INF/testdata/sample_schema_ddl_hsql.sql"), true);
}
public DeptVO makeVO() {
DeptVO vo = new DeptVO();
vo.setDeptNo(new BigDecimal(90));
vo.setDeptName("test 부서");
vo.setLoc("test 위치");
return vo;
}
public void checkResult(DeptVO vo, DeptVO resultVO) {
assertNotNull(resultVO);
assertEquals(vo.getDeptNo(), resultVO.getDeptNo());
assertEquals(vo.getDeptName(), resultVO.getDeptName());
assertEquals(vo.getLoc(), resultVO.getLoc());
}
@Test
public void testBasicInsert() throws Exception {
DeptVO vo = makeVO();
// insert
deptDAO.insertDept(vo);
// select
DeptVO resultVO = deptDAO.selectDept(vo);
// check
checkResult(vo, resultVO);
}
@Test
public void testBasicUpdate() throws Exception {
DeptVO vo = makeVO();
// insert
deptDAO.insertDept(vo);
// data change
vo.setDeptName("upd Dept");
vo.setLoc("upd loc");
// update
int effectedRows = deptDAO.updateDept(vo);
assertEquals(1, effectedRows);
// select
DeptVO resultVO = deptDAO.selectDept(vo);
// check
checkResult(vo, resultVO);
}
@Test
public void testBasicDelete() throws Exception {
DeptVO vo = makeVO();
// insert
deptDAO.insertDept(vo);
// delete
int effectedRows = deptDAO.deleteDept(vo);
assertEquals(1, effectedRows);
// select
DeptVO resultVO = deptDAO.selectDept(vo);
// null 이어야 함
assertNull(resultVO);
}
@Test
public void testBasicSelectList() throws Exception {
DeptVO vo = makeVO();
// insert
deptDAO.insertDept(vo);
// 검색조건으로 key 설정
DeptVO searchVO = new DeptVO();
searchVO.setDeptNo(new BigDecimal(90));
// selectList
List<DeptVO> resultList = deptDAO.selectDeptList(searchVO);
// key 조건에 대한 결과는 한건일 것임
assertNotNull(resultList);
assertTrue(resultList.size() > 0);
assertEquals(1, resultList.size());
checkResult(vo, resultList.get(0));
// 검색조건으로 name 설정 - '%' || #deptName# || '%'
DeptVO searchVO2 = new DeptVO();
searchVO2.setDeptName(""); // '%' || '' || '%' --> '%%'
// selectList
List<DeptVO> resultList2 = deptDAO.selectDeptList(searchVO2);
// like 조건에 대한 결과는 한건 이상일 것임
assertNotNull(resultList2);
assertTrue(resultList2.size() > 0);
}
}
기본적으로 Annotation 형식 Bean 생성 및 Dependency Injection 을 적용한 Spring 기반의 어플리케이션으로 구성하였으며, dataSource, transactionManager 등의 Spring Bean 이 함께 사용되고 있고, 테스트 케이스는 JUnit 4 형식으로 Spring 설정 파일 로딩 및 transactionManager, dataSource(DB 초기화를 위해 SimpleJdbcTemplate 사용 시 참조)를 얻을 수 있도록 되어 있음을 참고한다. 테스트 편의를 위해 매 테스트 메서드에 우선하여 @Before 로 정의된 메서드에서 기존 테이블 삭제 및 재생성 처리를 하고 있으며, makeVO 라는 별도 메서드로 테스트용 VO 작성 부분을 분리하고, checkResult 라는 메서드로 original VO 와 조회 결과 resultVO 에 대한 assert 비교 로직을 분리하여 재사용하고 있다. @Test 로 지시한 각 메서드가 테스트 메서드이고 입력-조회-결과체크, 입력-변경-조회-결과체크, 입력-삭제-조회-null체크, 검색조건 설정-조회-결과체크 의 flow 에 대한 검증으로 DeptDAO 에 대한 기본 CRUD 테스트 로직을 구성하였다.
※ 해당 프로젝트는 Hsqldb (현재 메모리구동 방식)을 사용하고 있으나 다른 DBMS 를 쓰고자 하는 경우 jdbc.properties 에 관련 접속정보를 추가하고 JDBC 드라이버 jar 파일을 라이브러리로 추가하여 테스트 해 볼 수 있다.
iBATIS 의 메인 설정 파일인 SQL Map XML Configuration 파일(이하 sql-map-config.xml 설정 파일) 작성과 상세한 옵션 설정에 대해 알아본다.
SqlMapClient 설정관련 상세 내역을 제어할 수 있는 메인 설정 파일로 주로 transaction 관리 관련 설정 및 다양한 옵션 설정, Sql Mapping 파일들에 대한 path 설정 등을 포함한다.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE sqlMapConfig PUBLIC "-//ibatis.apache.org//DTD SQL Map Config 2.0//EN"
"http://ibatis.apache.org/dtd/sql-map-config-2.dtd">
<sqlMapConfig>
<properties resource="META-INF/spring/jdbc.properties" />
<settings cacheModelsEnabled="true" enhancementEnabled="true"
lazyLoadingEnabled="true" maxRequests="128" maxSessions="10"
maxTransactions="5" useStatementNamespaces="false"
defaultStatementTimeout="1" />
<typeHandler javaType="java.util.Calendar" jdbcType="TIMESTAMP"
callback="egovframework.rte.psl.dataaccess.typehandler.CalendarTypeHandler" />
<transactionManager type="JDBC">
<dataSource type="DBCP">
<property name="driverClassName" value="${driver}" />
<property name="url" value="${dburl}" />
<property name="username" value="${username}" />
<property name="password" value="${password}" />
<!-- OPTIONAL PROPERTIES BELOW -->
<property name="maxActive" value="10" />
<property name="maxIdle" value="5" />
<property name="maxWait" value="60000" />
<!-- validation query -->
<!--<property name="validationQuery" value="select * from DUAL" />-->
<property name="logAbandoned" value="false" />
<property name="removeAbandoned" value="false" />
<property name="removeAbandonedTimeout" value="50000" />
<property name="Driver.DriverSpecificProperty" value="SomeValue" />
</dataSource>
</transactionManager>
<sqlMap resource="META-INF/sqlmap/mappings/testcase-basic.xml" />
<sqlMap ../>
..
</sqlMapConfig>
| 속성 | 설명 | Example, Default |
|---|---|---|
| maxRequests | 같은 시간대에 SQL 문을 실행한 수 있는 thread 의 최대 갯수 지정. | maxRequests=“256”, 512 |
| maxSessions | 주어진 시간에 활성화될 수 있는 session(또는 client) 수 지정. | maxSessions=“64”, 128 |
| maxTransactions | 같은 시간대에 SqlMapClient.startTransaction() 에 들어갈 수 있는 최대 갯수 지정. | maxTransactions=“16”, 32 |
| cacheModelsEnabled | SqlMapClient 에 대한 모든 cacheModel 에 대한 사용 여부를 global 하게 지정. | cacheModelsEnabled=“true”, true (enabled) |
| lazyLoadingEnabled | SqlMapClient 에 대한 모든 lazy loading 에 대한 사용 여부를 global 하게 지정. | lazyLoadingEnabled=“true”, true (enabled) |
| enhancementEnabled | runtime bytecode enhancement 기술 사용 여부 지정. | enhancementEnabled=“true”, false (disabled) |
| useStatementNamespaces | mapped statements 에 대한 참조 시 namespace 조합 사용 여부 지정. true 인 경우 queryForObject(“sqlMapName.statementName”); 과 같이 사용함. | useStatementNamespaces=“false”, false (disabled) |
| defaultStatementTimeout | 모든 JDBC 쿼리에 대한 timeout 시간(초) 지정, 각 statement 의 설정으로 override 가능함. 모든 driver가 이 설정을 지원하는 것은 아님에 유의할 것. | 지정하지 않는 경우 timeout 없음(cf. 각 statement 설정에 따라) |
| classInfoCacheEnabled | introspected(java 의 reflection API에 의해 내부 참조된) class의 캐쉬를 유지할지에 대한 설정 | classInfoCacheEnabled=“true”, true (enabled) |
| statementCachingEnabled | prepared statement 의 local cache 를 유지할지에 대한 설정 | statementCachingEnabled=“true”, true (enabled) |
이 외에도 typeAlias(global 한 type 별명-풀패키지명에 비해 간략히), resultObjectFactory (SQL 문의 실행에 의한 결과 객체의 생성을 iBATIS 의 ResultObjectFactory 인터페이스를 구현한 factory 클래스를 통해 처리할 수 있도록 지원) 에 대한 설정이 가능하다. DTD 상 sqlMap 설정은 하나 이상이 필요하고 다른 설정은 선택사항 이다.
sql 매핑 파일은 iBATIS 의 mapped statement 형태로 처리될 수 있도록 SQL Map 문서 구조에 따라 다양한 옵션 설정 및 매핑 정의, sql 문을 외부화하여 저장하는 파일이다.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE sqlMapConfig PUBLIC "-//ibatis.apache.org//DTD SQL Map Config 2.0//EN" "http://ibatis.apache.org/dtd/sql-map-config-2.dtd">
<sqlMap namespace="Dept">
<typeAlias alias="deptVO" type="egovframework.DeptVO" />
<resultMap id="deptResult" class="deptVO">
<result property="deptNo" column="DEPT_NO" />
<result property="deptName" column="DEPT_NAME" />
<result property="loc" column="LOC" />
</resultMap>
<insert id="insertDept" parameterClass="deptVO">
insert into DEPT
(DEPT_NO,
DEPT_NAME,
LOC)
values (#deptNo#,
#deptName#,
#loc#)
</insert>
<select id="selectDept" parameterClass="deptVO" resultMap="deptResult">
<![CDATA[
select DEPT_NO,
DEPT_NAME,
LOC
from DEPT
where DEPT_NO = #deptNo#
]]>
</select>
</sqlMap>
이 외에도 parameterMap, resultMap 에 대한 상세 정의, cacheModel 설정, sql 문 재사용을 위한 sql 요소 설정이 나타날 수 있다. 각각에 대한 상세 사항은 관련 가이드를 참고한다.
Data Mapper 사상의 핵심으로 Mapped Statement 는 parameter 매핑(input) 과 result 매핑(output) 을 가질 수 있는 어떤 SQL 문이라도 될 수 있다. 단순하게는 파라메터나 결과에 대한 class 를 직접적으로 설정할 수 있으며(권고하지 않는 방법이지만 아예 설정하지 않고 프레임워크에서 제공하는 자동 맵핑 처리도 가능), in/out 매핑, 결과의 cache 유지 등에 대한 상세한 설정이 가능하다.
<statement id="statementName"
[parameterClass="some.class.Name"]
[resultClass="some.class.Name"]
[parameterMap="nameOfParameterMap"]
[resultMap="nameOfResultMap"]
[cacheModel="nameOfCache"]
[timeout="5"]>
select * from PRODUCT where PRD_ID = [?|#propertyName#]
order by [$simpleDynamic$]
</statement>
Spring 프레임워크는 iBATIS SQL Map 을 이미 잘 통합하고 있으며, JDBC/Hibernate 에 대한 연동과 동일하게 template 스타일 프로그래밍이 가능토록 지원한다. 이러한 지원으로 Spring 의 특징인 IoC 의 장점과 Exception 계층 구조의 처리가 iBATIS 통합 환경에서도 쉽게 사용되고 있으며, iBATIS 단독 사용 시에 트랜잭션 관리 및 DataSource 에 대한 설정 및 관리가 별도로 필요했던 것에 비해 Spring-iBATIS 통합 환경에서는 Spring 의 유연한 트랜잭션 처리와 dataSource 를 그대로 사용할 수 있다.
SqlMapClientFactoryBean 은 iBATIS 의 SqlMapClient 를 생성하는 FactoryBean 구현체로, Spring 의 context 에 iBATIS 의 SqlMapClient 를 셋업하는 일반적인 방법으로 사용된다. 여기서 얻어진 SqlMapClient 는 iBATIS 기반 DAO 에 dependency injection 을 통해 넘겨지게 된다.
<!-- dataSource 설정 -->
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
<property name="driverClassName" value="${driver}"/>
<property name="url" value="${dburl}"/>
<property name="username" value="${username}"/>
<property name="password" value="${password}"/>
<property name="defaultAutoCommit" value="false"/>
<property name="poolPreparedStatements" value="true"/>
</bean>
<!-- SqlMap setup for iBATIS Database Layer -->
<bean id="sqlMapClient" class="egovframework.rte.psl.orm.ibatis.SqlMapClientFactoryBean">
<property name="configLocation" value="classpath:/META-INF/sqlmap/sql-map-config.xml"/>
<property name="dataSource" ref="dataSource"/>
</bean>
Spring 현재 버전에서는 configLocations 속성을 추가하여 sql-map-config.xml 에 대한 패턴 표현식이나 복수 연동(런타임에 하나의 통합 설정으로 merge 됨)도 지원하고 있다. useTransactionAwareDataSource 속성으로 SqlMapClient 에 대해 Spring 이 관리하는 transaction timeout 을 함께 적용할 수 있는 transaction-aware DataSource 를 사용하도록 설정 가능하며(default), lobHandler 속성을 통해 Spring 의 lobHandler 를 설정할 수도 있다.
또한 iBATIS 사용 환경에서의 중요한 개선 사항으로 mappingLocations 속성을 통해 기존에 iBATIS 메인 설정 파일 내에서 sqlMap 태그로 일일이 지정하여야만 했던 sql 매핑 파일에 대해 Spring 의 SqlMapClientFactoryBean 빈 설정파일에서 Spring 의 유연한 리소스 추상화를 적용하여 리소스 패턴 형태로 일괄 지정이 가능하다. 이 경우 sql 매핑 파일들의 위치는 sql-map-config 설정 파일과 런타임에 merge 되도록 세팅된다. 이 방법은 Spring 2.5.5 이상, iBATIS 2.3.2 이상에서만 지원됨에 유의한다.
<!-- SqlMap setup for iBATIS Database Layer -->
<bean id="sqlMapClient" class="egovframework.rte.psl.orm.ibatis.SqlMapClientFactoryBean">
<property name="configLocation"
value="classpath:/META-INF/sqlmap/sql-map-config.xml" />
<property name="mappingLocations"
value="classpath:/META-INF/sqlmap/mappings/testcase-*.xml" />
<property name="dataSource" ref="dataSource" />
</bean>
단 위와 같이 일괄 sql 매핑 파일 지정을 Spring 설정 파일에 지시하였더라도 iBATIS 의 sql-map-config.xml 의 DTD(http://ibatis.apache.org/dtd/sql-map-config-2.dtd) 에 sqlMap 태그가 최소 1개 이상이 나타나야 하도록 지정되어 있으므로 아래와 같이 dummy sql 매핑 파일 하나를 지정하는 sql-map-config.xml 로 작성하면 편할 것이다.
<!DOCTYPE sqlMapConfig PUBLIC "-//ibatis.apache.org//DTD SQL Map Config 2.0//EN"
"http://ibatis.apache.org/dtd/sql-map-config-2.dtd">
<sqlMapConfig>
<settings useStatementNamespaces="false"
defaultStatementTimeout="10"
/>
<typeHandler javaType="java.util.Calendar" jdbcType="TIMESTAMP"
callback="egovframework.rte.psl.dataaccess.typehandler.CalendarTypeHandler" />
<!-- Spring 2.5.5 이상, iBATIS 2.3.2 이상에서는 iBATIS 연동을 위한 SqlMapClientFactoryBean 정의 시
mappingLocations 속성으로 Sql 매핑 파일의 일괄 지정이 가능하다.
("sqlMapClient" bean 설정 시
mappingLocations="classpath:/META-INF/sqlmap/mappings/testcase-*.xml" 로 지정하였음)
단, sql-map-config-2.dtd 에서 sqlMap 요소를 하나 이상 지정하도록 되어 있으므로
아래의 dummy 매핑 파일을 설정하였다.
-->
<sqlMap resource="META-INF/sqlmap/mappings/testcase-dummy.xml" />
</sqlMapConfig>
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE sqlMap PUBLIC "-//iBATIS.com//DTD SQL Map 2.0//EN" "http://www.ibatis.com/dtd/sql-map-2.dtd">
<sqlMap namespace="Dummy"/>
Spring 의 SqlMapClientDaoSupport 클래스는 iBATIS 의 SqlMapClient data access object 를 위한 편리한 수퍼 클래스로 이를 extends 하는 서브 클래스에 SqlMapClientTemplate 를 제공한다. SqlMapClientTemplate 는 iBATIS 를 통한 data access 를 단순화하는 헬퍼 클래스로 SQLException 을 Spring dao 의 exception Hierarchy 에 맞게 unchecked DataAccessException 으로 변환해 주며 Spring 의 JdbcTemplate 과 동일한 처리 구조의 SQLExceptionTranslator 를 사용할 수 있게 해준다. 또한 iBATIS 의 SqlMapExecutor 의 실행 메서드에 대한 편리한 mirror 메서드를 다양하게 제공하므로 일반적인 쿼리나 insert/update/delete 처리에 대해 편리하게 사용할 수 있도록 권고된다. 그러나 batch update 와 같은 복잡한 수행에 대해서는 Spring 의 SqlMapClientCallback 에 대한 명시적인 구현(보통 anonymous inner class 로 작성)이 필요하다.
public class SqlMapAccountDao extends SqlMapClientDaoSupport implements AccountDao {
public Account getAccount(String email) throws DataAccessException {
return (Account) getSqlMapClientTemplate().queryForObject("getAccountByEmail", email);
}
public void insertAccount(Account account) throws DataAccessException {
getSqlMapClientTemplate().update("insertAccount", account);
}
}
iBATIS 연동 DAO 는 SqlMapClientDaoSupport 를 extends 하고 있으며, getSqlMapClientTemplate() 를 통해 SqlMapClientTemplate 를 얻어 iBATIS 의 data access 처리를 래핑하여 실행토록 처리하고 있다.
<bean id="accountDao" class="example.SqlMapAccountDao">
<property name="sqlMapClient" ref="sqlMapClient"/>
</bean>
iBATIS 연동 DAO 에 sqlMapClient 빈을 주입해 주어야 한다.
위의 처리는 Annotation 을 사용한 빈 생성 및 dependency 처리 시 sqlMapClient 의 DAO 주입 설정에 어려움이 존재한다. 이의 손쉬운 해결을 위해 전자정부 프레임워크는 EgovAbstractDAO 를 확장하여 제공하고 있다.
어플리케이션을 작성할 때 Data Type 에 대한 올바른 사용과 관련 처리는 매우 중요하다. 특히 데이터베이스를 이용하여 데이터를 저장하고 조회할 때 Java 어플리케이션에서의 Type 과 DBMS 에서 지원하는 관련 매핑 jdbc Type 의 정확한 사용이 필요하며, 여기에서는 iBATIS 환경에서 javaType 과 특정 DBMS 의 jdbcType 의 적절한 매핑 사용예를 중심으로 일반적인 Data Type 의 사용 가이드를 참고할 수 있도록 한다.
iBATIS SQL Mapper 프레임워크는 Java 어플리케이션 영역의 표준 JavaBeans 객체(또는 Map 등)의 각 Attribute 에 대한 Java Type 과 JDBC 드라이버에서 지원하는 각 DBMS의 테이블 칼럼에 대한 Data Type 의 매핑을 기반으로 parameter / result 객체에 대한 바인딩/매핑 을 처리한다. 각 javaType 에 대한 매칭되는 jdbcType 은 일반적인 Ansi SQL 을 사용한다고 하였을 때 아래에서 대략 확인할 수 있을 것이다. 특정 DBMS 벤더에 따라 추가적으로 지원/미지원 하는 jdbcType 이 다를 수 있고, 또한 같은 jdbcType 을 사용한다 하더라도 타입에 따른 사용 가능한 경계값(boundary max/min value)은 다를 수 있다.
아래에서는 다양한 primitive 타입과 숫자 타입, 문자 타입, 날짜 타입에 대한 기본 insert/select 를 통해 iBATIS 사용 환경에서의 data type 에 대한 사용 예를 알아보겠다.
public class TypeTestVO implements Serializable {
private static final long serialVersionUID = -3653247402772333834L;
private int id;
private BigDecimal bigdecimalType;
private boolean booleanType;
private byte byteType;
private String charType;
private double doubleType;
private float floatType;
private int intType;
private long longType;
private short shortType;
private String stringType;
private Date dateType;
private java.sql.Date sqlDateType;
private Time sqlTimeType;
private Timestamp sqlTimestampType;
private Calendar calendarType;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public BigDecimal getBigdecimalType() {
return bigdecimalType;
}
public void setBigdecimalType(BigDecimal bigdecimalType) {
this.bigdecimalType = bigdecimalType;
}
public boolean isBooleanType() {
return booleanType;
}
public void setBooleanType(boolean booleanType) {
this.booleanType = booleanType;
}
public byte getByteType() {
return byteType;
}
public void setByteType(byte byteType) {
this.byteType = byteType;
}
public String getCharType() {
return charType;
}
public void setCharType(String charType) {
this.charType = charType;
}
public double getDoubleType() {
return doubleType;
}
public void setDoubleType(double doubleType) {
this.doubleType = doubleType;
}
public float getFloatType() {
return floatType;
}
public void setFloatType(float floatType) {
this.floatType = floatType;
}
public int getIntType() {
return intType;
}
public void setIntType(int intType) {
this.intType = intType;
}
public long getLongType() {
return longType;
}
public void setLongType(long longType) {
this.longType = longType;
}
public short getShortType() {
return shortType;
}
public void setShortType(short shortType) {
this.shortType = shortType;
}
public String getStringType() {
return stringType;
}
public void setStringType(String stringType) {
this.stringType = stringType;
}
public Date getDateType() {
return dateType;
}
public void setDateType(Date dateType) {
this.dateType = dateType;
}
public java.sql.Date getSqlDateType() {
return sqlDateType;
}
public void setSqlDateType(java.sql.Date sqlDateType) {
this.sqlDateType = sqlDateType;
}
public Time getSqlTimeType() {
return sqlTimeType;
}
public void setSqlTimeType(Time sqlTimeType) {
this.sqlTimeType = sqlTimeType;
}
public Timestamp getSqlTimestampType() {
return sqlTimestampType;
}
public void setSqlTimestampType(Timestamp sqlTimestampType) {
this.sqlTimestampType = sqlTimestampType;
}
public Calendar getCalendarType() {
return calendarType;
}
public void setCalendarType(Calendar calendarType) {
this.calendarType = calendarType;
}
}
위 TypeTestVO 의 각 attribute 는 다양한 data Type 에 대한 사용예의 샘플이며 이에 대한 매핑 jdbc 타입은 아래의 각 DBMS 별 DDL 을 통해 일차적으로 살펴보자.
create table TYPETEST (
id numeric(10,0) not null,
bigdecimal_type numeric(19,2),
boolean_type boolean,
byte_type tinyint,
char_type char(1),
double_type double,
float_type float,
int_type integer,
long_type bigint,
short_type smallint,
string_type varchar(255),
date_type date,
sql_date_type datetime,
sql_time_type time,
sql_timestamp_type timestamp,
calendar_type timestamp,
primary key (id)
);
위 create sql 문의 hsqldb 의 예이며, 실제로 Ansi SQL 의 Data Type 에 대한 표준을 잘 따르는 예이다. boolean 타입을 직접 지원하고 있으며 tinyint, double, date, time 등 다양한 jdbcType 에 대하여 사용에 특별한 무리가 없음을 아래의 테스트 케이스로 알 수 있을 것이다.
<sqlMap namespace="TypeTest">
<typeAlias alias="typeTestVO"
type="egovframework.rte.psl.dataaccess.vo.TypeTestVO" />
<!-- CalendarTypeHandler 는 sql-map-config.xml 에 등록하였음 -->
<typeAlias alias="calendarTypeHandler" type="egovframework.rte.psl.dataaccess.typehandler.CalendarTypeHandler"/>
<resultMap id="typeTestResult" class="typeTestVO">
<result property="id" column="ID" />
<result property="bigdecimalType" column="BIGDECIMAL_TYPE" />
<result property="booleanType" column="BOOLEAN_TYPE" />
<result property="byteType" column="BYTE_TYPE" />
<result property="charType" column="CHAR_TYPE" />
<result property="doubleType" column="DOUBLE_TYPE" />
<result property="floatType" column="FLOAT_TYPE" />
<result property="intType" column="INT_TYPE" />
<result property="longType" column="LONG_TYPE" />
<result property="shortType" column="SHORT_TYPE" />
<result property="stringType" column="STRING_TYPE" />
<result property="dateType" column="DATE_TYPE" />
<result property="sqlDateType" column="SQL_DATE_TYPE" />
<result property="sqlTimeType" column="SQL_TIME_TYPE" />
<result property="sqlTimestampType" column="SQL_TIMESTAMP_TYPE" />
<result property="calendarType" column="CALENDAR_TYPE" typeHandler="calendarTypeHandler" />
</resultMap>
<insert id="insertTypeTest" parameterClass="typeTestVO">
<![CDATA[
insert into TYPETEST
(ID,
BIGDECIMAL_TYPE,
BOOLEAN_TYPE,
BYTE_TYPE,
CHAR_TYPE,
DOUBLE_TYPE,
FLOAT_TYPE,
INT_TYPE,
LONG_TYPE,
SHORT_TYPE,
STRING_TYPE,
DATE_TYPE,
SQL_DATE_TYPE,
SQL_TIME_TYPE,
SQL_TIMESTAMP_TYPE,
CALENDAR_TYPE)
values (#id#,
#bigdecimalType#,
#booleanType#,
#byteType#,
#charType:CHAR#,
#doubleType#,
#floatType#,
#intType#,
#longType#,
#shortType#,
#stringType#,
#dateType#,
#sqlDateType#,
#sqlTimeType#,
#sqlTimestampType#,
#calendarType,handler=calendarTypeHandler#)
]]>
</insert>
<select id="selectTypeTest" parameterClass="typeTestVO"
resultMap="typeTestResult">
<![CDATA[
select ID,
BIGDECIMAL_TYPE,
BOOLEAN_TYPE,
BYTE_TYPE,
CHAR_TYPE,
DOUBLE_TYPE,
FLOAT_TYPE,
INT_TYPE,
LONG_TYPE,
SHORT_TYPE,
STRING_TYPE,
DATE_TYPE,
SQL_DATE_TYPE,
SQL_TIME_TYPE,
SQL_TIMESTAMP_TYPE,
CALENDAR_TYPE
from TYPETEST
where ID = #id#
]]>
</select>
</sqlMap>
TypeTestVO JavaBeans 객체를 통해 insert/select 를 처리하는 sql 매핑 xml 이다. resultMap 을 정의하여 select 결과 객체 매핑을 처리하고 있으며, 입력 및 조회 조건 의 파라메터 바인딩을 Inline Parameter 방법을 통해 처리하고 있다. resultMap 이나 parameterMap(Inline Parameter 도 마찬가지) 에서는 javaType=“string”, jdbcType=“VARCHAR” 와 같이 명시적으로 java/jdbc type 에 대한 지시를 할 수도 있다. (성능상으로는 추천, 그러나 실제와 맞지 않는 type 지시는 런타임에 오류 발생) 또한, 위의 Inline parameter 처리 시 calendar 속성에 대해 handler=calendarTypeHandler 로 지시한 것과 resultMap 처리 시 typeHandler=“calendarTypeHandler” 로 지시한 것에서 확인할 수 있듯이 일반적인 java-jdbc 매핑으로 커버하지 못하는 부분에 대하여 사용자가 typeHandler 를 구현하여 타입 컨버전에 대한 로직 처리를 제공함으로써 위와 같이 calendar type ↔ TIMESTAMP 변환이 가능한 것처럼 확장할 수도 있다.
위에서 TypeTestVO 와 SQL Mapping XML 은 아래의 추가적인 DBMS 테스트 시 변경없이 재사용하였고, 일부 Data Type 의 미지원/DBMS 별 매핑타입 사용/경계값 상이함 등에 대해서는 DDL / TestCase 에서 약간의 로직 분기나 회피를 통해 문제되는 부분을 피하고 전체적인 관점에서 재사용 할 수 있도록 테스트 하였으므로 참고하기 바란다.
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {"classpath*:META-INF/spring/context-*.xml" })
@TransactionConfiguration(transactionManager = "txManager", defaultRollback = false)
@Transactional
public class DataTypeTest extends TestBase {
@Resource(name = "typeTestDAO")
TypeTestDAO typeTestDAO;
@Before
public void onSetUp() throws Exception {
// 외부에 sql file 로부터 DB 초기화 (TypeTest 기존 테이블 삭제/생성)
SimpleJdbcTestUtils.executeSqlScript(
new SimpleJdbcTemplate(dataSource), new ClassPathResource(
"META-INF/testdata/sample_schema_ddl_typetest_" + usingDBMS
+ ".sql"), true);
}
public TypeTestVO makeVO() throws Exception {
TypeTestVO vo = new TypeTestVO();
vo.setId(1);
vo.setBigdecimalType(new BigDecimal("99999999999999999.99"));
vo.setBooleanType(true);
vo.setByteType((byte) 127);
// VO 에서 String 으로 선언했음. char 로 하고자 하는 경우 TypeHandler 작성 필요
vo.setCharType("A");
// Oracle 10g 에서 double precision 타입은 Double.MAX_VALUE 를 수용하지 못함.
// oracle jdbc driver 에서 Double.MAX_VALUE 를 전달하면 Overflow Exception trying to bind 1.7976931348623157E308 에러 발생
// mysql 5.0 에서 테스트 시 Double.MAX_VALUE 입력은 가능하나 조회 시 Infinity 로 되돌려짐
// tibero - Double.MAX_VALUE 입력 시 Exception 발생
vo.setDoubleType(isHsql ? Double.MAX_VALUE : 1.7976931348623157d);
// mysql 5.0 에서 테스트 시 Float.MAX_VALUE 를 입력할 수 없음
vo.setFloatType(isMysql ? (float) 3.40282 : Float.MAX_VALUE);
vo.setIntType(Integer.MAX_VALUE);
vo.setLongType(Long.MAX_VALUE);
vo.setShortType(Short.MAX_VALUE);
vo.setStringType("abcd가나다라あいうえおカキクケコ");
SimpleDateFormat sdf =
new SimpleDateFormat("yyyy-MM-dd", java.util.Locale.getDefault());
vo.setDateType(sdf.parse("2009-02-18"));
long currentTime = new java.util.Date().getTime();
vo.setSqlDateType(new java.sql.Date(currentTime));
vo.setSqlTimeType(new java.sql.Time(currentTime));
vo.setSqlTimestampType(new java.sql.Timestamp(currentTime));
vo.setCalendarType(Calendar.getInstance());
return vo;
}
public void checkResult(TypeTestVO vo, TypeTestVO resultVO) {
assertNotNull(resultVO);
assertEquals(vo.getId(), resultVO.getId());
assertEquals(vo.getBigdecimalType(), resultVO.getBigdecimalType());
assertEquals(vo.getByteType(), resultVO.getByteType());
// mysql 인 경우 timestamp 칼럼에 null 을 입력하면 현재 시각으로 insert 됨에 유의
if (vo.getCalendarType() == null && isMysql) {
assertNotNull(resultVO.getCalendarType());
// mysql 인 경우 java 의 timestamp 에 비해 3자리 정밀도 떨어짐
} else if (vo.getCalendarType() != null && isMysql) {
String orgSeconds =
Long.toString(vo.getCalendarType().getTime().getTime());
String mysqlSeconds =
Long.toString(resultVO.getCalendarType().getTime().getTime());
assertEquals(orgSeconds.substring(0, orgSeconds.length() - 3),
mysqlSeconds.substring(0, mysqlSeconds.length() - 3));
} else {
assertEquals(vo.getCalendarType(), resultVO.getCalendarType());
}
assertEquals(vo.getCharType(), resultVO.getCharType());
assertEquals(vo.getDateType(), resultVO.getDateType());
// double 에 대한 delta 를 1e-15 로 주었음.
assertEquals(vo.getDoubleType(), resultVO.getDoubleType(), isMysql
? 1e-14 : 1e-15);
// float 에 대한 delta 를 1e-7 로 주었음.
assertEquals(vo.getFloatType(), resultVO.getFloatType(), 1e-7);
assertEquals(vo.getIntType(), resultVO.getIntType());
assertEquals(vo.getLongType(), resultVO.getLongType());
assertEquals(vo.getShortType(), resultVO.getShortType());
// java.sql.Date 의 경우 Date 만 비교
if (vo.getSqlDateType() != null) {
assertEquals(vo.getSqlDateType().toString(), resultVO
.getSqlDateType().toString());
}
// mysql 인 경우 timestamp 칼럼에 null 을 입력하면 현재 시각으로 insert 됨에 유의
if (vo.getSqlTimestampType() == null && isMysql) {
assertNotNull(resultVO.getSqlTimestampType());
} else if (vo.getCalendarType() != null && isMysql) {
String orgSeconds =
Long.toString(vo.getSqlTimestampType().getTime());
String mysqlSeconds =
Long.toString(resultVO.getSqlTimestampType().getTime());
assertEquals(orgSeconds.substring(0, orgSeconds.length() - 3),
mysqlSeconds.substring(0, mysqlSeconds.length() - 3));
} else {
assertEquals(vo.getSqlTimestampType(), resultVO
.getSqlTimestampType());
}
// java.sql.Time 의 경우 Time 만 비교
if ((isHsql || isOracle || isTibero || isMysql)
&& vo.getSqlTimeType() != null) {
assertEquals(vo.getSqlTimeType().toString(), resultVO
.getSqlTimeType().toString());
} else {
assertEquals(vo.getSqlTimeType(), resultVO.getSqlTimeType());
}
assertEquals(vo.getStringType(), resultVO.getStringType());
assertEquals(vo.isBooleanType(), resultVO.isBooleanType());
}
@Test
public void testDataTypeTest() throws Exception {
// 값을 세팅하지 않고 insert 해 봄 - id 는 int 의 초기값에 따라 0 임
TypeTestVO vo = new TypeTestVO();
// insert
typeTestDAO.insertTypeTest("insertTypeTest", vo);
// select
TypeTestVO resultVO = typeTestDAO.selectTypeTest("selectTypeTest", vo);
// check
checkResult(vo, resultVO);
try {
// duplication 테스트
typeTestDAO.insertTypeTest("insertTypeTest", vo);
fail("키 값 duplicate 에러가 발생해야 합니다.");
} catch (Exception e) {
assertNotNull(e);
assertTrue(e instanceof DataIntegrityViolationException);
assertTrue(e.getCause() instanceof SQLException);
}
// DataType 테스트 데이터 입력 및 재조회
vo = makeVO();
// insert
typeTestDAO.insertTypeTest("insertTypeTest", vo);
// select
resultVO = typeTestDAO.selectTypeTest("selectTypeTest", vo);
// check
checkResult(vo, resultVO);
}
}
위에서는 TypeTestVO 의 각 속성에 값을 세팅하지 않고 입력/조회, 키 값 중복 시 spring 의 DataIntegrityViolationException 이 발생하는지, 각 속성에 테스트 데이터(경계값 또는 의미있는 사용예 로써의 값)를 세팅하여 입력/조회 에 대한 처리를 확인해 봄으로써 java ↔ DBMS 의 타입 매핑의 예를 확인해 보았다. 특히 위의 makeVO 메서드 에서는 특정 javaType 에 대한 DBMS 의 db type 에 따라 경계값의 max value 가 달라질 수 있음을 확인할 수 있으며, checkResult 메서드에서는 특히 날짜 처리 타입과 관련하여 DBMS 에 따라 null 입력일 때 초기값 이나, 지원하는 정밀도(입력시 java 의 Date 류에서는 년월일시분초 를 넘어 상세하게 표현한 입력값 javaType 에 대해 jdbcType 의 결과 조회 시 날짜, 또는 시각 정보만으로 제한된다던지, 초 레벨의 정밀도가 java 에 비해 낮다던지)의 차이가 있음을 확인할 수 있다. java-jdbc type 에 대한 일반적인 매핑은 위 Hsqldb 의 예를 기본으로 이해하면 적합할 것으로 보며, 아래에서 특정 DBMS 의 DDL 예를 통해 각 데이터베이스 환경에서 Data Type 사용의 참고가 될 수 있기 바란다.
create table TYPETEST (
id number(10,0) not null,
bigdecimal_type number(19,2),
boolean_type number(1,0),
byte_type number(3,0),
char_type char(1),
double_type double precision,
float_type float,
int_type number(10,0),
long_type number(19,0),
short_type number(5,0),
string_type varchar2(255),
date_type date,
sql_date_type date,
sql_time_type timestamp,
sql_timestamp_type timestamp,
calendar_type timestamp,
primary key (id)
);
create table TYPETEST (
id numeric(10,0) not null,
bigdecimal_type numeric(19,2),
boolean_type boolean,
byte_type tinyint,
char_type char(1),
double_type double,
float_type float,
int_type integer,
long_type bigint,
short_type smallint,
string_type varchar(255),
date_type date,
sql_date_type datetime,
sql_time_type time,
sql_timestamp_type timestamp,
calendar_type timestamp,
primary key (id)
);
create table TYPETEST (
id numeric(10,0) not null,
bigdecimal_type number(19,2),
boolean_type number(1),
byte_type number(3),
char_type char(1),
double_type double precision,
float_type float,
int_type integer,
long_type integer, /* integer 가 bigint 의 범위까지 포함함 */
short_type smallint,
string_type varchar(255),
date_type date,
sql_date_type date,
sql_time_type date, /* time, timestamp 으로 설정 시 문제발생 */
sql_timestamp_type timestamp,
calendar_type timestamp,
primary key (id)
);
parameterMap 은 해당 요소로 SQL 문 외부에 정의한 입력 객체의 속성에 대한 name 및 javaType, jdbcType 을 비롯한 옵션을 설정할 수 있는 매핑 요소이다. 이를 통해 JavaBeans 객체(또는 Map 등)에 대한 prepared statement 에 대한 바인드 변수 매핑을 처리할 수 있다. 유사한 기능을 처리하는 parameterClass 나 Inline Parameter 에 비해 많이 사용되지 않지만 더 기술적인(descriptive) parameterMap(예를 들어 stored procedure 를 위한) 이 필요하거나, XML 의 일관된 사용과 순수성을 지키고자 할때 좋은 접근법이 될 수도 있다. 그러나 Dynamic 요소와 함께 사용될 수 없고 바인드 변수의 갯수와 순서를 정확히 맞춰야 하는 불편이 있는 등 일반적으로 사용을 추천하지 않는다.
아래의 샘플 parameterMap 정의를 참고하라.
..
<typeAlias alias="empVO" type="egovframework.rte.psl.dataaccess.vo.EmpVO" />
<parameterMap id="empParam" class="empVO">
<parameter property="empNo" javaType="decimal" jdbcType="NUMERIC" />
<parameter property="empName" javaType="string" jdbcType="VARCHAR" nullValue="blank" />
<parameter property="job" javaType="string" jdbcType="VARCHAR" nullValue="" />
<parameter property="mgr" javaType="decimal" jdbcType="NUMERIC" />
<parameter property="hireDate" javaType="date" jdbcType="DATE" />
<parameter property="sal" javaType="decimal" jdbcType="NUMERIC" />
<parameter property="comm" javaType="decimal" jdbcType="NUMERIC" nullValue="-99999" />
<parameter property="deptNo" javaType="decimal" jdbcType="NUMERIC" />
</parameterMap>
<insert id="insertEmpUsingParameterMap" parameterMap="empParam">
<![CDATA[
insert into EMP
(EMP_NO,
EMP_NAME,
JOB,
MGR,
HIRE_DATE,
SAL,
COMM,
DEPT_NO)
values (?,
?,
?,
?,
?,
?,
?,
?)
]]>
</insert>
위 sql 매핑 파일에서 parameterMap 요소로 empParam 이라는 id 를 부여하고 대상 입력 객체는 EmpVO 를 지정하고 있다. EmpVO 에 대한 상세 attribute (속성) 들에 대해 parameter 하위 요소로 매핑 정의를 하고 있는데 이때 추가적으로 javaType, jdbcType 에 대해 위에서는 명시하였다. (java 의 reflection 기술을 사용하여 대상 클래스의 개별 속성에 대한 type 을 구하는 것보다 직접 type 에 대한 지시를 설정으로 명시하므로 약간의 성능상 이점이 있을 수 있다.) 위에서는 동일한 property 가 중복으로 사용되는 경우가 없으나 parameterMap 은 아래의 insertEmpUsingParameterMap mapped statement 예에서 보듯이 ? 에 대한 순서대로 매핑되므로 만약 중복으로 사용되는 경우가 필요하다면 parameterMap 정의부터 순서를 맞추어 동일한 property 의 중복 정의가 필요할 수 있다. 또한 nullValue 속성을 지정한 property 에 대해서는 해당 값이 nullValue 에 지정된 값으로 전달되는 경우 데이터베이스에는 null 로 처리가 이 외에도 typeName, resultMap, mode, typeHandler, numericScale 에 대한 속성 정의가 가능하다.
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {"classpath*:META-INF/spring/context-*.xml" })
@TransactionConfiguration(transactionManager = "txManager", defaultRollback = false)
@Transactional
public class ParameterMapTest extends TestBase {
@Resource(name = "empDAO")
EmpDAO empDAO;
@Before
public void onSetUp() throws Exception {
// DB 초기화
}
public EmpVO makeVO() throws ParseException {
EmpVO vo = new EmpVO();
vo.setEmpNo(new BigDecimal(9000));
vo.setEmpName("test Emp");
vo.setJob("test Job");
// 7839,'KING','PRESIDENT'
vo.setMgr(new BigDecimal(7839));
SimpleDateFormat sdf =
new SimpleDateFormat("yyyy-MM-dd", java.util.Locale.getDefault());
vo.setHireDate(sdf.parse("2009-02-09"));
// mysql 5.0.X에서는 소숫점 자릿수 만큼 .00 이 달려 나와 테스트 편의상 소숫점 자리수가 없도록 칼럼 선언 하였음.
if (isMysql) {
vo.setSal(new BigDecimal("12345"));
vo.setComm(new BigDecimal(100));
} else {
vo.setSal(new BigDecimal("12345.67"));
vo.setComm(new BigDecimal(100.00));
}
// 10,'ACCOUNTING','NEW YORK'
vo.setDeptNo(new BigDecimal(10));
return vo;
}
public void checkResult(EmpVO vo, EmpVO resultVO) {
assertNotNull(resultVO);
assertEquals(vo.getEmpNo(), resultVO.getEmpNo());
assertEquals(vo.getEmpName(), resultVO.getEmpName());
assertEquals(vo.getJob(), resultVO.getJob());
assertEquals(vo.getMgr(), resultVO.getMgr());
assertEquals(vo.getHireDate(), resultVO.getHireDate());
assertEquals(vo.getSal(), resultVO.getSal());
assertEquals(vo.getComm(), resultVO.getComm());
assertEquals(vo.getDeptNo(), resultVO.getDeptNo());
}
@Test
public void testParameterMapInsert() throws Exception {
EmpVO vo = makeVO();
// insert
empDAO.insertEmp("insertEmpUsingParameterMap", vo);
// select
EmpVO resultVO = empDAO.selectEmp("selectEmp", vo);
// check
checkResult(vo, resultVO);
}
@Test
public void testParameterMapInsertWithNullValue() throws Exception {
EmpVO vo = new EmpVO();
// key 설정
vo.setEmpNo(new BigDecimal(9000));
// parameterMap nullValue test
vo.setEmpName("blank");
vo.setJob("");
// cf.) -99999.99 는 NumberFormatException 임을
// 확인하였음!
vo.setComm(new BigDecimal("-99999"));
// insert
empDAO.insertEmp("insertEmpUsingParameterMap", vo);
// select
EmpVO resultVO = empDAO.selectEmp("selectEmp", vo);
// check
assertNotNull(resultVO);
assertEquals(vo.getEmpNo(), resultVO.getEmpNo());
// parameterMap 설정에서 nullValue="blank" .. 에 따라
// 해당값이 null 로 입력되었을 것임
assertNull(resultVO.getEmpName());
assertNull(resultVO.getJob());
assertNull(resultVO.getComm());
}
}
위에서 parameterMap 을 이용한 파라메터 객체 바인딩을 처리하는 insertEmpUsingParameterMap 쿼리문에 대해 입력객체를 세팅 후 입력/조회 처리하고 있는 테스트 케이스이다. 실제로 매핑 파일 내에서 parameterMap 을 사용하는 경우와 Inline parameter 또는 parameterClass 를 그대로 쓰는 경우에서도 어플리케이션 영역은 동일한 형태의 입력 객체를 인자로 iBATIS API 를 호출하게 된다. 위의 testParameterMapInsertWithNullValue 테스트 메서드에서는 parameterMap 에 nullValue 속성으로 지정한 특정한 값을 세팅 하므로써 DB 에 null 로 입력이 된 결과를 조회하여 assertNull 로 확인하고 있다.
이전에 살펴본 prepared statement 에 대한 바인드 변수 매핑 처리를 위한 parameterMap 요소(SQL 문 외부에 정의한 입력 객체 property name 및 javaType, jdbcType 을 비롯한 옵션을 설정 매핑 요소) 와 동일한 기능을 처리하는 간편한 방법을 Inline Parameters 방법으로 제공한다. 보통 parameterClass 로 명시된 입력 객체에 대해 바인드 변수 영역을 간단한 #property# 노테이션으로 나타내는 Inline Parameter 방법은 기존 parameterMap 에서의 ? 와 이의 순서를 맞춘 외부 parameterMap 선언으로 처리하는 방법에 비해 많이 사용되고 일반적으로 추천하는 방법이다. 이는 Dynamic 요소와 함께 사용될 수 있고 별도의 외부 매핑 정의 없이 바인드 변수 처리가 필요한 위치에 해당 property 를 직접 사용 가능하며, 필요한 경우 jdbcType 이나 nullValue 를 간단한 추가 노테이션(ex. #empName:VARCHAR:blank# ) 와 같이 지정할 수 있고, 상세한 옵션이 필요한 경우에는 (ex. #comm,javaType=decimal,jdbcType=NUMERIC,nullValue=-99999# ) 와 같이 ,(comma) 로 구분된 필요한 속성=값 을 상세하게 기술할 수도 있다.
아래의 샘플 Inline Parameters 사용 쿼리문을 참고하라.
..
<typeAlias alias="empVO" type="egovframework.rte.psl.dataaccess.vo.EmpVO" />
<insert id="insertEmptUsingInLineParam">
<![CDATA[
insert into EMP
(EMP_NO,
EMP_NAME,
JOB,
MGR,
HIRE_DATE,
SAL,
COMM,
DEPT_NO)
values (#empNo:NUMERIC#,
#empName:VARCHAR:blank#,
#job:VARCHAR:""#, /* inline parameter 에서는 empty String 을 nullValue로 대체할 수 없음 - cf.) oracle인 경우는 "" 가 null 임 */
#mgr:NUMERIC#,
#hireDate:DATE#,
#sal:NUMERIC#,
#comm,javaType=decimal,jdbcType=NUMERIC,nullValue=-99999#,
#deptNo:NUMERIC#)
]]>
</insert>
위 sql 매핑 파일에서 별도의 parameterMap 외부 정의없이 #property:jdbcType# 형태로 직접 바인드 변수 영역에 나타내고 있다. #property# 만 나타내도 iBATIS 에서 자동으로 타입처리는 잘된다. 위에서는 해당 쿼리에 parameterClass=“empVO” 에 대한 입력 객체 설정이 나타나 있지 않음에도 iBATIS 에서 런타임에 인자로 주어지는 입력 객체를 자동으로 파악하여 파라메터 처리를 하고 있음을 확인할 수 있다. 그러나 parameterClass 의 명시는 성능 상 반드시 추천하는 바이다.
이 외에도 속성=값 형태의 옵션 설정을 , 를 구분자로 아래와 같이 나타낼 수 있다.
위에서 ? 부분을 해당 속성에 따라 적절한 값으로 설정하면 된다. mode 는 stored procedure 의 IN/OUT/INOUT 모드를 지시할 수 있는 속성이고 numericScale 은 stored procedure 의 OUT/INOUT 변수가 decimal 이나 numeric인 경우 DBMS 의 Scale 정보를 유지하기 위해 명시해야 하는 속성이다. handler 속성에는 typeHandler 를 지시할 수 있다.
..
public EmpVO makeVO() throws ParseException {
EmpVO vo = new EmpVO();
vo.setEmpNo(new BigDecimal(9000));
vo.setEmpName("test Emp");
vo.setJob("test Job");
// 7839,'KING','PRESIDENT'
vo.setMgr(new BigDecimal(7839));
SimpleDateFormat sdf =
new SimpleDateFormat("yyyy-MM-dd", java.util.Locale.getDefault());
vo.setHireDate(sdf.parse("2009-02-09"));
// mysql 은 소숫점 이하 자리가 .00 으로 기본 들어가게 되어 테스트 편의상 numeric(5) 로 선언하였음.
if (isMysql) {
vo.setSal(new BigDecimal("12345"));
vo.setComm(new BigDecimal(100));
} else {
vo.setSal(new BigDecimal("12345.67"));
vo.setComm(new BigDecimal(100.00));
}
// 10,'ACCOUNTING','NEW YORK'
vo.setDeptNo(new BigDecimal(10));
return vo;
}
public void checkResult(EmpVO vo, EmpVO resultVO) {
assertNotNull(resultVO);
assertEquals(vo.getEmpNo(), resultVO.getEmpNo());
assertEquals(vo.getEmpName(), resultVO.getEmpName());
assertEquals(vo.getJob(), resultVO.getJob());
assertEquals(vo.getMgr(), resultVO.getMgr());
assertEquals(vo.getHireDate(), resultVO.getHireDate());
assertEquals(vo.getSal(), resultVO.getSal());
assertEquals(vo.getComm(), resultVO.getComm());
assertEquals(vo.getDeptNo(), resultVO.getDeptNo());
}
@Test
public void testInLineParameterInsert() throws Exception {
EmpVO vo = makeVO();
// insert
empDAO.insertEmp("insertEmptUsingInLineParam", vo);
// select
EmpVO resultVO = empDAO.selectEmp("selectEmp", vo);
// check
checkResult(vo, resultVO);
}
@Test
public void testInLineParameterInsertWithNullValue() throws Exception {
EmpVO vo = new EmpVO();
// key 설정
vo.setEmpNo(new BigDecimal(9000));
// inline parameter nullValue test
vo.setEmpName("blank");
// inline parameter 에서는 empty String 을
// nullValue로 대체할 수 없음
// ref.)
// http://www.nabble.com/inline-map-format%3A-empty-String-in-nullValue-td18905940.html
vo.setJob(""); // cf.) oracle 인 경우 "" 는 null 과
// 같음!
vo.setComm(new BigDecimal("-99999"));
// insert
empDAO.insertEmp("insertEmptUsingInLineParam", vo);
// select
EmpVO resultVO = empDAO.selectEmp("selectEmp", vo);
// check
assertNotNull(resultVO);
assertEquals(vo.getEmpNo(), resultVO.getEmpNo());
// inline parameter 설정에서
// #empName:VARCHAR:blank# ..
// 에 따라 해당값이 null 로
// 입력되었을 것임
assertNull(resultVO.getEmpName());
// inline parameter 에서는 empty String 을
// nullValue로 대체할 수 없음 확인!
// cf.) parameterMap 케이스에서는
// assertNull(resultVO.getJob()) // cf.) oracle
// 인 경우 "" 는 null 과 같음!
// assertNotNull(resultVO.getJob());
assertNull(resultVO.getComm());
}
..
위에서 Inline Parameters 을 이용한 파라메터 객체 바인딩을 처리하는 insertEmptUsingInLineParam 쿼리문에 대해 입력객체를 세팅 후 입력/조회 처리하고 있는 테스트 케이스이다. 실제로 위 어플리케이션 영역은 parameterMap 사용 시와 차이가 없음을 알 수 있다. 위의 testInLineParameterInsertWithNullValue 테스트 메서드에서는 Inline Parameters 에 nullValue 속성으로 지정한 특정한 값을 세팅 함으로써 DB 에 null 로 입력이 된 결과를 조회하여 assertNull 로 확인하고 있다. 위에서 empty String 의 nullValue 설정은 inline parameter 노테이션의 한계로 제대로 처리되지 않음을 확인하였으므로 참고하기 바란다.
resultMap 은 SQL 문 외부에 정의한 매핑 요소로, result set 으로부터 어떻게 데이터를 뽑아낼지, 어떤 칼럼을 어떤 property로 매핑할지에 대한 상세한 제어를 가능케 해준다. resultMap 은 일반적으로 가장 많이 사용되는 중요한 매핑 요소로 resultClass 속성을 이용한 자동 매핑 접근법에 비교하여 칼럼 타입의 지시, null value 대체값, typeHandler 처리, complex property 매핑(다른 JavaBean, Collections 등을 포함하는 복합 객체) 등을 허용한다.
아래의 샘플 resultMap 정의를 참고하라.
..
<typeAlias alias="empVO" type="egovframework.rte.psl.dataaccess.vo.EmpVO" />
<resultMap id="empResult" class="empVO" >
<result property="empNo" column="EMP_NO" columnIndex="1"
javaType="decimal" jdbcType="NUMERIC" />
<result property="empName" column="EMP_NAME" columnIndex="2"
javaType="string" jdbcType="VARCHAR" />
<result property="job" column="JOB" columnIndex="3" javaType="string"
jdbcType="VARCHAR" />
<result property="mgr" column="MGR" columnIndex="4" javaType="decimal"
jdbcType="NUMERIC" />
<result property="hireDate" column="HIRE_DATE" columnIndex="5"
javaType="date" jdbcType="DATE" />
<result property="sal" column="SAL" columnIndex="6" javaType="decimal"
jdbcType="NUMERIC" />
<result property="comm" column="COMM" columnIndex="7" javaType="decimal"
jdbcType="NUMERIC" nullValue="0" />
<result property="deptNo" column="DEPT_NO" columnIndex="8"
javaType="decimal" jdbcType="NUMERIC" />
</resultMap>
<select id="selectEmpUsingResultMap" parameterClass="empVO" resultMap="empResult">
<![CDATA[
select EMP_NO,
EMP_NAME,
JOB,
MGR,
HIRE_DATE,
SAL,
COMM,
DEPT_NO
from EMP
where EMP_NO = #empNo#
]]>
</select>
위 sql 매핑 파일에서 resultMap 요소로 empResult 라는 id 를 부여하고 대상 결과 객체는 EmpVO 를 지정하고 있다. EmpVO 에 대한 상세 attribute (속성) 들에 대해 result 하위 요소로 매핑 정의를 하고 있는데 column 속성으로 result set 에서 얻을 수 있는 select 대상 칼럼(column alias 를 쓴 경우이면 해당 alias 명) 을 매핑하게 된다. 위에서는 추가적으로 columnIndex, javaType, jdbcType 에 대해 명시하였다. 위와 같이 타입을 명확하게 지시해주면 java 의 reflection 기술을 사용하여 대상 클래스의 개별 속성에 대한 type 을 구하는 것보다 성능상 이점이 있을 수 있다. columnIndex 를 지정하는 경우에는 rs.getString(“EMP_NAME”) → rs.getString(2)로 처리되는 사소한 성능상의 이점이 있지만, 순서의 지정이나 설정 자체의 번거로움으로 추천하지 않는 바이다. 또한 nullValue 속성을 지정한 property 에 대해서는 해당 값이 데이터베이스에서 null 로 읽혔을 때 nullValue 에 지정된 값으로 대체되어 JavaBeans property 에 설정된다. 이 외에도 result 하위 요소의 속성으로 select, resultMap 을 통해 다른 쿼리문의 결과나 complex property 의 처리를 위한 내포 객체에 대한 외부 resultMap 매핑요소를 참조할 수 있다. 또한 typeHandler 속성을 통해 iBATIS 의 기본 처리가 아닌 custom typeHandler 구현체를 지시할 수도 있다.
<resultMap id="resultMapName" class="some.domain.Class"
[extends="parent-resultMap"] [groupBy="some property list"]>
<result property="propertyName" column="COLUMN_NAME"
[columnIndex="1"] [javaType="int"] [jdbcType="NUMERIC"] [nullValue="-999999"]
[select="someOtherStatement"] [resultMap="someOtherResultMap"]
[typeHandler="com.mydomain.MyTypehandler"] />
<result … />
<result … />
<result … />
</resultMap>
위에서 resultMap 태그의 extends 속성을 명시하면 외부에 정의한 다른 resultMap 을 상속(관련 property - column 매핑을 현재 resultMap 에 정의하지 않고도)할 수 있으며, groupBy 속성을 사용하여 nested resultMap 에서의 N+1 쿼리 문제를 풀수 있도록 해당 속성을 통해 명시한 property 리스트의 값이 같은 row 들에 대해 하나의 결과 객체로 생성해 주게 된다.
..
@Test
public void testResultMapSelect() throws Exception {
EmpVO vo = new EmpVO();
// 7369,'SMITH','CLERK',7902,'1980-12-17',800,NULL,20
vo.setEmpNo(new BigDecimal(7369));
// select
EmpVO resultVO = empDAO.selectEmp("selectEmpUsingResultMap", vo);
// check
assertNotNull(resultVO);
assertEquals(new BigDecimal(7369), resultVO.getEmpNo());
assertEquals("SMITH", resultVO.getEmpName());
assertEquals("CLERK", resultVO.getJob());
assertEquals(new BigDecimal(7902), resultVO.getMgr());
SimpleDateFormat sdf =
new SimpleDateFormat("yyyy-MM-dd", java.util.Locale.getDefault());
assertEquals(sdf.parse("1980-12-17"), resultVO.getHireDate());
assertEquals(new BigDecimal(800), resultVO.getSal());
// nullValue test - <result property="comm" column="COMM" .. nullValue="0" />
assertEquals(new BigDecimal(0), resultVO.getComm());
assertEquals(new BigDecimal(20), resultVO.getDeptNo());
}
위에서 resultMap 을 이용한 결과 객체 매핑을 처리하는 selectEmpUsingResultMap 쿼리문에 대해 조회조건(pk) 를 세팅한 입력 객체를 인자로 조회 처리하고 있는 테스트 케이스이다. 실제로 매핑 파일 내에서 resultMap 을 사용하는 경우와 resultClass 를 그대로 쓰는 경우에서도 어플리케이션 영역은 동일한 형태의 결과 객체를 얻을 수 있지만 sql 매핑 파일내에서 resultMap 의 정의와 사용은 많은 장점이 있으므로 추천된다. 위의 테스트 검증을 통해 EmpVO 의 결과 객체로 조회 결과가 잘 반영되었음을 확인할 수 있고 nullValue 속성으로 지정한 comm attribute 에 대해서는 데이터베이스는 null 이지만 0 에 해당하는 numeric 값으로 대체되어 조회되었음을 확인할 수 있다.
아래의 샘플 resultMap 정의를 참고하라.
..
public class EmpExtendsDeptVO extends DeptVO {
private static final long serialVersionUID = -4653117983538108612L;
private BigDecimal empNo;
private String empName;
private String job;
private BigDecimal mgr;
private Date hireDate;
private BigDecimal sal;
private BigDecimal comm;
public BigDecimal getEmpNo() {
return empNo;
}
public void setEmpNo(BigDecimal empNo) {
this.empNo = empNo;
}
..
..
<typeAlias alias="empExtendsDeptVO" type="egovframework.rte.psl.dataaccess.vo.EmpExtendsDeptVO" />
<!--
cf.) VO 의 상속관계와 resultMap의 상속관계가 같을 필요는 없음. 아래의 empExtendsDeptResult 가
empResult (Emp 속성을 가지고 있는 위의 resultMap)를 extends 하고 있지만 실제
EmpExtendsDeptVO 는 DeptVO 를 extends 하면서 child가 Emp 속성을 가지게끔 define 했음을 볼
수 있음
-->
<resultMap id="empExtendsDeptResult" class="empExtendsDeptVO" extends="empResult">
<!--<result property="deptNo" column="DEPT_NO"/>-->
<result property="deptName" column="DEPT_NAME"/>
<result property="loc" column="LOC"/>
</resultMap>
<select id="selectEmpExtendsDeptUsingResultMap" parameterClass="empVO" resultMap="empExtendsDeptResult">
<![CDATA[
select EMP_NO,
EMP_NAME,
JOB,
MGR,
HIRE_DATE,
SAL,
COMM,
A.DEPT_NO,
B.DEPT_NAME,
B.LOC
from EMP A, DEPT B
where A.DEPT_NO = B.DEPT_NO
and EMP_NO = #empNo#
]]>
</select>
empExtendsDeptResult resultMap 은 위의 기본적인 resultMap 사용 방법에서 정의한 empResult resultMap 을 extends 하고 있으며, 추가적인 deptName, loc 에 대한 property 매핑만을 추가하여 실제로 emp 관련 property 들에 대해서는 extends 하고 있는 매핑 정의를 따라 자동으로 처리가 됨을 확인할 수 있다. resultMap 에 대한 매핑 정의의 상속은 결과 객체 JavaBeans 의 상속 관계와는 상관없이 별개로 이루어진다. (위에서는 VO extends 코드와 resultMap extends 가 반대임)
..
@Test
public void testExtendsResultMapSelect() throws Exception {
EmpVO vo = new EmpVO();
// 7369,'SMITH','CLERK',7902,'1980-12-17',800,NULL,20
vo.setEmpNo(new BigDecimal(7369));
// select
EmpExtendsDeptVO resultVO =
empDAO.selectEmpExtendsDept("selectEmpExtendsDeptUsingResultMap",
vo);
// check
assertNotNull(resultVO);
// resultMap extends test (extends empResult)
assertEquals(new BigDecimal(7369), resultVO.getEmpNo());
assertEquals("SMITH", resultVO.getEmpName());
assertEquals("CLERK", resultVO.getJob());
assertEquals(new BigDecimal(7902), resultVO.getMgr());
SimpleDateFormat sdf =
new SimpleDateFormat("yyyy-MM-dd", java.util.Locale.getDefault());
assertEquals(sdf.parse("1980-12-17"), resultVO.getHireDate());
assertEquals(new BigDecimal(800), resultVO.getSal());
// nullValue test - <result property="comm" column="COMM" .. nullValue="0" />
assertEquals(new BigDecimal(0), resultVO.getComm());
assertEquals(new BigDecimal(20), resultVO.getDeptNo());
assertEquals("RESEARCH", resultVO.getDeptName());
assertEquals("DALLAS", resultVO.getLoc());
}
위에서 extends 속성을 통해 상위 resultMap 을 상속하는 empExtendsDeptResult resultMap 을 이용한 결과 객체 매핑을 처리하는 selectEmpExtendsDeptUsingResultMap 에 대해 조회 처리하고 있는 테스트 케이스이다. 위의 테스트 검증을 통해 EmpExtendsDeptVO 의 결과 객체로 조회 결과가 잘 반영되었음을 확인할 수 있고 parent resultMap 에 존재하는 nullValue 대체 처리도 잘 반영됨을 확인할 수 있다.
아래의 샘플 resultMap 정의를 참고하라.
..
public class EmpDeptSimpleCompositeVO implements Serializable {
private static final long serialVersionUID = -8049578957221741495L;
private BigDecimal empNo;
private String empName;
private String job;
private BigDecimal mgr;
private Date hireDate;
private BigDecimal sal;
private BigDecimal comm;
private BigDecimal deptNo;
private String deptName;
private String loc;
public BigDecimal getEmpNo() {
return empNo;
}
public void setEmpNo(BigDecimal empNo) {
this.empNo = empNo;
}
..
별도의 parent - child extends 구조를 사용하지 않고 단순히 attributes 를 통합한 VO 이다.
..
<typeAlias alias="empDeptSimpleCompositeVO" type="egovframework.rte.psl.dataaccess.vo.EmpDeptSimpleCompositeVO" />
<resultMap id="empDeptSimpleComposite" class="empDeptSimpleCompositeVO" >
<result property="empNo" column="EMP_NO"/>
<result property="empName" column="EMP_NAME"/>
<result property="job" column="JOB"/>
<result property="mgr" column="MGR"/>
<result property="hireDate" column="HIRE_DATE"/>
<result property="sal" column="SAL"/>
<result property="comm" column="COMM" nullValue="0"/>
<result property="deptNo" column="DEPT_NO"/>
<result property="deptName" column="DEPT_NAME"/>
<result property="loc" column="LOC"/>
</resultMap>
<select id="selectEmpDeptSimpleCompositeUsingResultMap" parameterClass="empVO" resultMap="empDeptSimpleComposite">
<![CDATA[
select EMP_NO,
EMP_NAME,
JOB,
MGR,
HIRE_DATE,
SAL,
COMM,
A.DEPT_NO,
B.DEPT_NAME,
B.LOC
from EMP A, DEPT B
where A.DEPT_NO = B.DEPT_NO
and EMP_NO = #empNo#
]]>
</select>
EMP 와 DEPT 에 대한 조인 쿼리를 통하여 DEPT 정보를 포함하는 결과 row 를 조회하는 쿼리문은 위에서와 완전히 동일하며, extends 를 사용하는 empExtendsDeptResult resultMap 과 비교하여 resultMap 정의가 모든 요소를 포함하고 있다. 위에서는 조회 필드가 결과 객체의 property 가 동일하므로 extends 를 사용하는 resultMap 으로 단순히 resultClass 만 맞춰주어 변경 가능할 것이다.
..
@Test
public void testSimpleCompositeResultMapSelect() throws Exception {
EmpVO vo = new EmpVO();
// 7369,'SMITH','CLERK',7902,'1980-12-17',800,NULL,20
vo.setEmpNo(new BigDecimal(7369));
// select
EmpDeptSimpleCompositeVO resultVO =
empDAO.selectEmpDeptSimpleComposite(
"selectEmpDeptSimpleCompositeUsingResultMap", vo);
// check
assertNotNull(resultVO);
assertEquals(new BigDecimal(7369), resultVO.getEmpNo());
assertEquals("SMITH", resultVO.getEmpName());
assertEquals("CLERK", resultVO.getJob());
assertEquals(new BigDecimal(7902), resultVO.getMgr());
SimpleDateFormat sdf =
new SimpleDateFormat("yyyy-MM-dd", java.util.Locale.getDefault());
assertEquals(sdf.parse("1980-12-17"), resultVO.getHireDate());
assertEquals(new BigDecimal(800), resultVO.getSal());
// nullValue test - <result property="comm" column="COMM" .. nullValue="0" />
assertEquals(new BigDecimal(0), resultVO.getComm());
assertEquals(new BigDecimal(20), resultVO.getDeptNo());
assertEquals("RESEARCH", resultVO.getDeptName());
assertEquals("DALLAS", resultVO.getLoc());
}
extends 없이 모든 매핑 요소를 simple 하게 모두 정의한 resultMap 을 이용한 결과 객체 매핑을 처리하는 selectEmpDeptSimpleCompositeUsingResultMap 에 대해 조회 처리하고 있는 테스트 케이스이다. 위의 테스트 검증을 통해 EmpDeptSimpleCompositeV 의 결과 객체로 조회 결과가 잘 반영되었음을 확인할 수 있다.
위의 비교 구현을 통해 join 조회를 통해 얻어지는 결과 객체가 단순 composite VO 형태인 경우 resultMap extends 을 사용하면 좀더 쉽게 매핑 처리할 수 있을 것으로 보여진다.
아래의 1:1, 1:N, 1:N(N+1 select), Hierarchical relation 에 대한 샘플 resultMap 정의를 참고하라.
..
public class EmpIncludesDeptVO implements Serializable {
private static final long serialVersionUID = -4113989804152701350L;
private BigDecimal empNo;
private String empName;
private String job;
private BigDecimal mgr;
private Date hireDate;
private BigDecimal sal;
private BigDecimal comm;
private BigDecimal deptNo;
// EMP - DEPT 1:1 relation
private DeptVO deptVO;
public BigDecimal getEmpNo() {
return empNo;
}
public void setEmpNo(BigDecimal empNo) {
this.empNo = empNo;
}
..
public DeptVO getDeptVO() {
return deptVO;
}
public void setDeptVO(DeptVO deptVO) {
this.deptVO = deptVO;
}
}
위의 EmpIncludesDeptVO 는 DeptVO 를 1:1 관계의 멤버 attribute 로 포함하고 있다.
<sqlMap namespace="EmpComplexResult">
..
<typeAlias alias="empIncludesDeptVO" type="egovframework.rte.psl.dataaccess.vo.EmpIncludesDeptVO" />
<resultMap id="empIncludesDeptResult" class="empIncludesDeptVO">
<result property="empNo" column="EMP_NO" />
<result property="empName" column="EMP_NAME" />
<result property="job" column="JOB" />
<result property="mgr" column="MGR" />
<result property="hireDate" column="HIRE_DATE" />
<result property="sal" column="SAL" />
<result property="comm" column="COMM" nullValue="0" />
<result property="deptNo" column="DEPT_NO" />
<!--
Emp-Dept 1:1 relation
테스트 결과 resultMap 의 참조 시 sql-map-config.xml 의
useStatementNamespaces="false" 와 상관없이 namespace prefix 를 써야 하는듯
-->
<result property="deptVO" resultMap="EmpComplexResult.getDeptResult" />
</resultMap>
<resultMap id="getDeptResult" class="deptVO">
<result property="deptNo" column="DEPT_NO" />
<result property="deptName" column="DEPT_NAME" />
<result property="loc" column="LOC" />
</resultMap>
<select id="selectEmpIncludesDeptResultUsingResultMap" parameterClass="empVO" resultMap="empIncludesDeptResult">
<![CDATA[
select EMP_NO,
EMP_NAME,
JOB,
MGR,
HIRE_DATE,
SAL,
COMM,
A.DEPT_NO as DEPT_NO,
B.DEPT_NAME,
B.LOC
from EMP A, DEPT B
where A.DEPT_NO = B.DEPT_NO
and EMP_NO = #empNo#
]]>
</select>
위 sql 매핑 파일에서 1:1 관계를 표현하는 Complex Properties 를 포함하는 EmpIncludesDeptVO 에 대한 resultMap 매핑 처리 시 해당 객체의 deptVO 멤버 attribute 에 대한 매핑 정의를 위해 resultMap=“EmpComplexResult.getDeptResult” 과 같이 DeptVO 에 대한 외부 resultMap 을 재사용하며 참조하고 있다. iBATIS 는 nested resultMap 에 대한 매핑 정의를 참고하여 join 쿼리에 의한 결과 칼럼을 자동으로 복합 객체(특히 DeptVO 관련 멤버 객체에)에 매핑해 주게 된다.
..
@Test
public void testComplexPropertiesOneToOneResultMapSelect() throws Exception {
EmpVO vo = new EmpVO();
// 7369,'SMITH','CLERK',7902,'1980-12-17',800,NULL,20
vo.setEmpNo(new BigDecimal(7369));
// select
EmpIncludesDeptVO resultVO =
empDAO.selectEmpDeptComplexProperties(
"selectEmpIncludesDeptResultUsingResultMap", vo);
// check
assertNotNull(resultVO);
assertEquals(new BigDecimal(7369), resultVO.getEmpNo());
assertEquals("SMITH", resultVO.getEmpName());
assertEquals("CLERK", resultVO.getJob());
assertEquals(new BigDecimal(7902), resultVO.getMgr());
SimpleDateFormat sdf =
new SimpleDateFormat("yyyy-MM-dd", java.util.Locale.getDefault());
assertEquals(sdf.parse("1980-12-17"), resultVO.getHireDate());
assertEquals(new BigDecimal(800), resultVO.getSal());
assertEquals(new BigDecimal(0), resultVO.getComm());
assertEquals(new BigDecimal(20), resultVO.getDeptNo());
// 1:1 relation included DeptVO
assertEquals(new BigDecimal(20), resultVO.getDeptVO().getDeptNo());
assertEquals("RESEARCH", resultVO.getDeptVO().getDeptName());
assertEquals("DALLAS", resultVO.getDeptVO().getLoc());
}
위의 테스트 케이스 검증 코드에서 resultVO.getDeptVO().getXXX 와 같이 내포 객체의 각 attribute 가 잘 설정되었음을 확인할 수 있다.
..
public class DeptIncludesEmpListVO implements Serializable {
private static final long serialVersionUID = -3369530755443065377L;
private BigDecimal deptNo;
private String deptName;
private String loc;
private List<EmpVO> empVOList;
public BigDecimal getDeptNo() {
return deptNo;
}
public void setDeptNo(BigDecimal deptNo) {
this.deptNo = deptNo;
}
..
public List<EmpVO> getEmpVOList() {
return empVOList;
}
public void setEmpVOList(List<EmpVO> empVOList) {
this.empVOList = empVOList;
}
}
위의 DeptIncludesEmpListVO 는 EmpVO 의 List 를 1:N 관계의 멤버 attribute 로 포함하고 있다.
<sqlMap namespace="EmpComplexResult">
..
<typeAlias alias="deptIncludesEmpListVO" type="egovframework.rte.psl.dataaccess.vo.DeptIncludesEmpListVO" />
<!-- 1:N 인 경우 groupBy 속성을 명시 -->
<resultMap id="deptIncludesEmpListResult" class="deptIncludesEmpListVO" groupBy="deptNo">
<result property="deptNo" column="DEPT_NO" />
<result property="deptName" column="DEPT_NAME" />
<result property="loc" column="LOC" />
<!-- Dept-EmpList 1:N relation -->
<result property="empVOList" resultMap="EmpComplexResult.getEmpResult" />
</resultMap>
<resultMap id="getEmpResult" class="empVO">
<result property="empNo" column="EMP_NO" />
<result property="empName" column="EMP_NAME" />
<result property="job" column="JOB" />
<result property="mgr" column="MGR" />
<result property="hireDate" column="HIRE_DATE" />
<result property="sal" column="SAL" />
<result property="comm" column="COMM" nullValue="0" />
<result property="deptNo" column="DEPT_NO" />
</resultMap>
<select id="selectDeptIncludesEmpListResultUsingResultMap" parameterClass="deptVO" resultMap="deptIncludesEmpListResult">
<![CDATA[
select A.DEPT_NO as DEPT_NO,
DEPT_NAME,
LOC,
EMP_NO,
EMP_NAME,
JOB,
MGR,
HIRE_DATE,
SAL,
COMM
from DEPT A,
EMP B
where A.DEPT_NO = B.DEPT_NO
and A.DEPT_NO = #deptNo#
order by B.EMP_NO
]]>
</select>
위 sql 매핑 파일에서 1:N 관계를 표현하는 Complex Properties 를 포함하는 DeptIncludesEmpListVO 에 대한 resultMap 매핑 처리 시 해당 객체의 empVOList 멤버 attribute 에 대한 매핑 정의를 위해 resultMap=“EmpComplexResult.getEmpResult” 과 같이 EmpVO 에 대한 외부 resultMap 을 재사용하며 참조하고 있다. 이때 위의 쿼리에서는 join 에 따라 조회되는 row 수가 1:1 관계와 같이 단건이 아니라 같은 DEPT_NO 에 대해 복수 개의 결과가 얻어지는데 이에 대한 resultMap 정의 시 groupBy=“deptNo” 을 지정하였으므로 같은 deptNo 인 다건의 EmpVO 에 대한 List 가 복합 객체의 List 멤버 attribute 에 설정되어 얻어진다. iBATIS 는 nested resultMap 에 대한 매핑 정의를 참고하여 join 쿼리에 의한 결과 칼럼을 자동으로 복합 객체에 매핑해 주게 되며 여기에서와 같이 groupBy 로 지정한 property 로 그룹핑하여 하위 요소를 List 형태로 자동 세팅할 수 있다.
..
@Test
public void testComplexPropertiesOneToManyResultMapSelect()
throws Exception {
DeptVO vo = new DeptVO();
// 20,'RESEARCH','DALLAS'
vo.setDeptNo(new BigDecimal(20));
// select
DeptIncludesEmpListVO resultVO =
empDAO.selectDeptEmpListComplexProperties(
"selectDeptIncludesEmpListResultUsingResultMap", vo);
// check
assertNotNull(resultVO);
assertEquals(new BigDecimal(20), resultVO.getDeptNo());
assertEquals("RESEARCH", resultVO.getDeptName());
assertEquals("DALLAS", resultVO.getLoc());
assertTrue(0 < resultVO.getEmpVOList().size());
/*
* deptNo 20 인 EmpList 는 초기데이터에 따라 7369,'SMITH','CLERK',7902,'1980-12-17',800,NULL,20
* 7566,'JONES','MANAGER',7839,'1981-04-02',2975,NULL,20 7788,'SCOTT','ANALYST',7566,'1987-04-19',3000,NULL,20
* 7876,'ADAMS','CLERK',7788,'1987-05-23',1100,NULL,20 7902,'FORD','ANALYST',7566,'1981-12-03',3000,NULL,20
*/
assertEquals(5, resultVO.getEmpVOList().size());
assertEquals(new BigDecimal(7369), resultVO.getEmpVOList().get(0)
.getEmpNo());
assertEquals("SMITH", resultVO.getEmpVOList().get(0).getEmpName());
assertEquals("CLERK", resultVO.getEmpVOList().get(0).getJob());
assertEquals(new BigDecimal(7902), resultVO.getEmpVOList().get(0)
.getMgr());
SimpleDateFormat sdf =
new SimpleDateFormat("yyyy-MM-dd", java.util.Locale.getDefault());
assertEquals(sdf.parse("1980-12-17"), resultVO.getEmpVOList().get(0)
.getHireDate());
assertEquals(new BigDecimal(800), resultVO.getEmpVOList().get(0)
.getSal());
assertEquals(new BigDecimal(0), resultVO.getEmpVOList().get(0)
.getComm());
assertEquals(new BigDecimal(20), resultVO.getEmpVOList().get(0)
.getDeptNo());
assertEquals(new BigDecimal(7566), resultVO.getEmpVOList().get(1)
.getEmpNo());
assertEquals(new BigDecimal(7788), resultVO.getEmpVOList().get(2)
.getEmpNo());
assertEquals(new BigDecimal(7876), resultVO.getEmpVOList().get(3)
.getEmpNo());
assertEquals(new BigDecimal(7902), resultVO.getEmpVOList().get(4)
.getEmpNo());
}
위의 테스트 케이스 검증 코드에서 resultVO.getEmpVOList().get(X).getXXX 와 같이 내포 객체(List<EmpVO>)의 각 EmpVO 와 해당 attributes 가 잘 설정되었음을 확인할 수 있다.
1:N 관계를 포함하는 복합 객체의 리스트를 조회할 때 outer join 을 사용한 예이다.
<sqlMap namespace="EmpComplexResult">
..
<select id="selectDeptIncludesEmpListResultListUsingResultMap" parameterClass="deptVO" resultMap="deptIncludesEmpListResult">
<![CDATA[
select A.DEPT_NO as DEPT_NO,
DEPT_NAME,
LOC,
EMP_NO,
EMP_NAME,
JOB,
MGR,
HIRE_DATE,
SAL,
COMM
from DEPT A
left outer join EMP B
on (A.DEPT_NO = B.DEPT_NO)
where A.DEPT_NAME like '%'||#deptName#||'%'
order by A.DEPT_NO,
B.EMP_NO
]]>
</select>
..
@Test
public void testComplexPropertiesOneToManyVOListResultMapSelect()
throws Exception {
DeptVO vo = new DeptVO();
// deptName 에 의한 like 검색 테스트 '%'|| 'E' ||'%' --> R'E'S'E'ARCH, SAL'E'S, OP'E'RATIONS
// 20,'RESEARCH','DALLAS'
// 30,'SALES','CHICAGO'
// 40,'OPERATIONS','BOSTON'
vo.setDeptName("E");
// select
List<DeptIncludesEmpListVO> resultList =
empDAO.selectDeptEmpListComplexPropertiesList(isMysql
? "selectDeptIncludesEmpListResultListUsingResultMapMysql"
: "selectDeptIncludesEmpListResultListUsingResultMap", vo);
// check
assertNotNull(resultList);
assertEquals(3, resultList.size());
assertEquals(new BigDecimal(20), resultList.get(0).getDeptNo());
assertEquals(new BigDecimal(30), resultList.get(1).getDeptNo());
assertEquals(new BigDecimal(40), resultList.get(2).getDeptNo());
/*
* deptNo 20 인 EmpList 는 초기데이터에 따라 5 명, deptNo 30 인 EmpList 는 초기데이터에 따라 6 명, deptNo 40 인 EmpList 는 초기데이터에 따라 0 명
* --> cf.)outer join 에 따라 deptNo 만 가진 EmpVO 1건 생김
*/
assertEquals(5, resultList.get(0).getEmpVOList().size());
assertEquals(6, resultList.get(1).getEmpVOList().size());
// cf.)outer join 에 따라 deptNo 만 가진 EmpVO 1건 생김을 확인함. 주의할 것!
assertEquals(1, resultList.get(2).getEmpVOList().size());
assertNull(resultList.get(2).getEmpVOList().get(0).getEmpNo());
/*
* deptNo 20 인 EmpList 는 초기데이터에 따라 7369,'SMITH','CLERK',7902,'1980-12-17',800,NULL,20
* 7566,'JONES','MANAGER',7839,'1981-04-02',2975,NULL,20 7788,'SCOTT','ANALYST',7566,'1987-04-19',3000,NULL,20
* 7876,'ADAMS','CLERK',7788,'1987-05-23',1100,NULL,20 7902,'FORD','ANALYST',7566,'1981-12-03',3000,NULL,20
*/
assertEquals(new BigDecimal(7566), resultList.get(0).getEmpVOList()
.get(1).getEmpNo());
assertEquals(new BigDecimal(7788), resultList.get(0).getEmpVOList()
.get(2).getEmpNo());
assertEquals(new BigDecimal(7876), resultList.get(0).getEmpVOList()
.get(3).getEmpNo());
assertEquals(new BigDecimal(7902), resultList.get(0).getEmpVOList()
.get(4).getEmpNo());
}
위의 테스트 케이스 검증 코드에서 iBATIS 의 resultMap 을 사용하여 outer join 의 결과를 복합 객체로 매핑하는 경우 (groupBy 속성 사용 형태) join key 에 해당하는 값만을 가진 하위 객체가 의도하지 않게 생기는 문제가 있어 보이므로 사용에 유의하기 바란다!
EmpVO 의 List 를 1:N 관계의 멤버 attribute 로 포함하고 있는 DeptIncludesEmpListVO 를 결과 객체로 사용하고 있는 것은 같다.
<sqlMap namespace="EmpComplexResult">
..
<!-- 1:N 인 경우 N+1 select 형태 - 비추천 -->
<resultMap id="deptIncludesEmpListUsingSelectAttrResult" class="deptIncludesEmpListVO">
<result property="deptNo" column="DEPT_NO" />
<result property="deptName" column="DEPT_NAME" />
<result property="loc" column="LOC" />
<!-- Dept-EmpList 1:N relation using select attribute -->
<result property="empVOList" column="DEPT_NO" select="selectEmpList" />
</resultMap>
<select id="selectDeptIncludesEmpListResultListUsingRepetitionSelect" parameterClass="deptVO"
resultMap="deptIncludesEmpListUsingSelectAttrResult">
<![CDATA[
select DEPT_NO,
DEPT_NAME,
LOC
from DEPT
where DEPT_NAME like '%'||#deptName#||'%'
order by DEPT_NO
]]>
</select>
<select id="selectEmpList" parameterClass="decimal" resultMap="getEmpResult">
<![CDATA[
select EMP_NO,
EMP_NAME,
JOB,
MGR,
HIRE_DATE,
SAL,
COMM,
DEPT_NO
from EMP
where DEPT_NO = #deptNo#
order by EMP_NO
]]>
</select>
위 sql 매핑 파일에서 1:N 관계를 표현하는 Complex Properties 를 포함하는 DeptIncludesEmpListVO 에 대한 resultMap 매핑 처리 시 해당 객체의 empVOList 멤버 attribute 에 대한 매핑 정의를 위해 select=“selectEmpList” 과 같이 별도의 쿼리문을 호출하여(EmpVO 에 대한 resultMap 으로 처리되는) 처리하는 예이다. 쿼리문 상에서 join 을 사용하지 않고 있으며 메인 쿼리문 한번(1번)의 호출에도 결과 rows 수만큼의 select 속성으로 지정한 별도 쿼리 (N번) 가 수행되므로 성능 측면에서 매우 바람직하지 않은 형태이다.
..
@Test
public void testComplexPropertiesOneToManyVOListRepetitionSelect()
throws Exception {
DeptVO vo = new DeptVO();
// deptName 에 의한 like 검색 테스트 '%'|| 'E' ||'%' --> R'E'S'E'ARCH, SAL'E'S, OP'E'RATIONS
// 20,'RESEARCH','DALLAS'
// 30,'SALES','CHICAGO'
// 40,'OPERATIONS','BOSTON'
vo.setDeptName("E");
// select
List<DeptIncludesEmpListVO> resultList =
empDAO
.selectDeptEmpListComplexPropertiesList(
isMysql
? "selectDeptIncludesEmpListResultListUsingRepetitionSelectMysql"
: "selectDeptIncludesEmpListResultListUsingRepetitionSelect",
vo);
// check
assertNotNull(resultList);
assertEquals(3, resultList.size());
assertEquals(new BigDecimal(20), resultList.get(0).getDeptNo());
assertEquals(new BigDecimal(30), resultList.get(1).getDeptNo());
assertEquals(new BigDecimal(40), resultList.get(2).getDeptNo());
/*
* deptNo 20 인 EmpList 는 초기데이터에 따라 5 명, deptNo 30 인 EmpList 는 초기데이터에 따라 6 명 deptNo 40 인 EmpList 는 초기데이터에 따라 0 명
* --> 위 outer join 케이스와 달리 EmpList 도 건수 없음 확인
*/
assertEquals(5, resultList.get(0).getEmpVOList().size());
assertEquals(6, resultList.get(1).getEmpVOList().size());
assertEquals(0, resultList.get(2).getEmpVOList().size());
/*
* deptNo 20 인 EmpList 는 초기데이터에 따라 7369,'SMITH','CLERK',7902,'1980-12-17',800,NULL,20
* 7566,'JONES','MANAGER',7839,'1981-04-02',2975,NULL,20 7788,'SCOTT','ANALYST',7566,'1987-04-19',3000,NULL,20
* 7876,'ADAMS','CLERK',7788,'1987-05-23',1100,NULL,20 7902,'FORD','ANALYST',7566,'1981-12-03',3000,NULL,20
*/
assertEquals(new BigDecimal(7566), resultList.get(0).getEmpVOList()
.get(1).getEmpNo());
assertEquals(new BigDecimal(7788), resultList.get(0).getEmpVOList()
.get(2).getEmpNo());
assertEquals(new BigDecimal(7876), resultList.get(0).getEmpVOList()
.get(3).getEmpNo());
assertEquals(new BigDecimal(7902), resultList.get(0).getEmpVOList()
.get(4).getEmpNo());
}
위의 테스트 케이스 검증 코드에서 resultList.get(X).getEmpVOList().get(X).getXXX 와 같이 조회 결과(List) 의 내포 객체(List<EmpVO>)의 각 EmpVO 와 해당 attributes 가 잘 설정되었음을 확인할 수 있다. 그러나 N+1 조회의 성능 문제를 야기하므로 sql 매핑 정의 시 join 쿼리와 groupBy 속성을 통한 1:N 관계 처리 매핑으로 처리하는 것이 바람직하다.
..
public class EmpIncludesMgrVO implements Serializable {
private static final long serialVersionUID = 5695339933191681519L;
private BigDecimal empNo;
private String empName;
private String job;
private BigDecimal mgr;
private Date hireDate;
private BigDecimal sal;
private BigDecimal comm;
private BigDecimal deptNo;
// Hierarchy 관계
private EmpIncludesMgrVO mgrVO;
public BigDecimal getEmpNo() {
return empNo;
}
public void setEmpNo(BigDecimal empNo) {
this.empNo = empNo;
}
..
public EmpIncludesMgrVO getMgrVO() {
return mgrVO;
}
public void setMgrVO(EmpIncludesMgrVO mgrVO) {
this.mgrVO = mgrVO;
}
}
위의 EmpIncludesMgrVO 는 자신과 동일한 EmpIncludesMgrVO 를 Hierarchy 관계의 멤버 attribute 로 포함하고 있다. 위에서는 Emp 의 Manager 에 대해서 포함하고 있는데 MgrVO 를 거슬러 올라가면 결국 자기 관리자 path 에 있는 모든 사원에 대한 정보를 포함하고 있는 객체라 볼 수 있다.
<sqlMap namespace="EmpComplexResult">
..
<typeAlias alias="empIncludesMgrVO" type="egovframework.rte.psl.dataaccess.vo.EmpIncludesMgrVO" />
<!-- Hierarchical relation 인 경우 ibatis resultMap select 에 의한 처리 예 -->
<resultMap id="empIncludesMgrResult" class="empIncludesMgrVO" extends="getEmpResult">
<result property="mgrVO" column="MGR" select="selectMgrHierarchy" />
</resultMap>
<!-- 하나의 쿼리를 사용하여 empNo, mgr 속성의 유무에 따라 Hierarchy 반복조회로도 사용토록 변경
parameterClass 는 최초 조회와 resultMap 의 select 조회의 경우 empVO 와 decimal 로 다르기 때문에
따로 명시하지 않고 자동 reflection 에 의해 처리토록 함
-->
<select id="selectMgrHierarchy" resultMap="empIncludesMgrResult">
<![CDATA[
select EMP_NO,
EMP_NAME,
JOB,
MGR,
HIRE_DATE,
SAL,
COMM,
DEPT_NO
from EMP
where 1=1
]]>
<!-- 최초 - empNo 는 parameter bean 의 property 임-->
<isPropertyAvailable property="empNo" prepend="and">
EMP_NO = #empNo#
</isPropertyAvailable>
<!-- 반복 - column="MGR" 에 의한 연결로 empNo 는 property가 아님 -->
<isNotPropertyAvailable property="empNo" prepend="and">
EMP_NO = #mgr#
</isNotPropertyAvailable>
</select>
위 sql 매핑 파일에서 Hierarchy 관계를 표현하는 Complex Properties 를 포함하는 EmpIncludesMgrVO 에 대한 resultMap 매핑 처리 시 해당 객체의 mgrVO 멤버 attribute 에 대한 매핑 정의를 위해 select=“selectMgrHierarchy” 과 같이 자기 자신의 sql 문을 재호출 하는 형태이다.
..
@Test
public void testComplexPropertiesHierarcyRepetitionSelect()
throws Exception {
EmpVO vo = new EmpVO();
// 7369,'SMITH','CLERK',7902
// --> 7902,'FORD','ANALYST',7566
// --> 7566,'JONES','MANAGER',7839
// --> 7839,'KING','PRESIDENT',NULL
vo.setEmpNo(new BigDecimal(7369));
try {
// select
// EmpIncludesMgrVO resultVO =
// empDAO.selectEmpMgrHierarchy("selectEmpWithMgr", vo);
EmpIncludesMgrVO resultVO =
empDAO.selectEmpMgrHierarchy("selectMgrHierarchy", vo);
// check
assertNotNull(resultVO);
assertEquals(new BigDecimal(7369), resultVO.getEmpNo());
assertEquals("SMITH", resultVO.getEmpName());
assertEquals("CLERK", resultVO.getJob());
assertEquals(new BigDecimal(7902), resultVO.getMgr());
SimpleDateFormat sdf =
new SimpleDateFormat("yyyy-MM-dd", java.util.Locale
.getDefault());
assertEquals(sdf.parse("1980-12-17"), resultVO.getHireDate());
assertEquals(new BigDecimal(800), resultVO.getSal());
assertEquals(new BigDecimal(0), resultVO.getComm());
assertEquals(new BigDecimal(20), resultVO.getDeptNo());
assertTrue(resultVO.getMgrVO() instanceof EmpIncludesMgrVO);
assertEquals(new BigDecimal(7902), resultVO.getMgrVO().getEmpNo());
assertEquals(new BigDecimal(7566), resultVO.getMgrVO().getMgrVO()
.getEmpNo());
assertEquals(new BigDecimal(7839), resultVO.getMgrVO().getMgrVO()
.getMgrVO().getEmpNo());
assertNull(resultVO.getMgrVO().getMgrVO().getMgrVO().getMgrVO());
} catch (UncategorizedSQLException ue) {
// tibero 인 경우 ibatis 의 재귀 queyr 형태의 sub 객체 맵핑 시 com.tmax.tibero.jdbc.TbSQLException: TJDBC-90646:Resultset
// is already closed 에러 발생 확인함!
assertTrue(isTibero);
assertTrue(ue.getCause() instanceof NestedSQLException);
assertTrue(ue.getCause().getCause().getCause().getCause() instanceof TbSQLException);
assertTrue(((TbSQLException) ue.getCause().getCause().getCause()
.getCause()).getMessage().contains(
"TJDBC-90646:Resultset is already closed"));
}
}
위의 테스트 케이스 검증 코드에서 SMITH → FORD → JONES → KING 으로 연결되는 관리자 정보를 모두 포함하는 복합 객체의 각 attribute 가 잘 설정되었음을 확인할 수 있다. 일부 DBMS 에서는 문제가 발생할 수 있으므로 사용에 유의한다.
일반적으로 JDBC API 를 사용한 코딩에서 한번 정의한 쿼리문을 최대한 재사용하고자 하나 단순 파라메터 변수의 값만 변경하는 것으로 해결하기 어렵고 다양한 조건에 따라 조금씩 다른 쿼리의 실행이 필요한 경우 많은 if~else 조건 분기의 연결이 필요한 문제가 있다. 여기에서는 SQL 문의 동적인 변경에 대한 상대적으로 유연한 방법을 제공하는 iBATIS 의 Dynamic 요소에 대해 알아본다.
아래의 샘플 Dynamic 요소 사용예를 참고하라.
..
<typeAlias alias="jobHistVO" type="egovframework.rte.psl.dataaccess.vo.JobHistVO" />
<select id="selectJobHistListUsingDynamicElement" parameterClass="jobHistVO" resultClass="jobHistVO">
<![CDATA[
select EMP_NO as empNo,
START_DATE as startDate,
END_DATE as endDate,
JOB as job,
SAL as sal,
COMM as comm,
DEPT_NO as deptNo
from JOBHIST
]]>
<dynamic prepend="where">
<isNotNull property="empNo" prepend="and">
EMP_NO = #empNo#
</isNotNull>
</dynamic>
order by EMP_NO, START_DATE
</select>
위 sql 매핑 파일에서 파라메터 객체의 empNo 속성의 값 유무에 따라 where EMP_NO = #empNo# 조건절이 동적으로 추가/제거 될 수 있는 예이다. 위에서 dynamic 의 prepend 속성으로 “where” 를 지정하고 있지만 하위 요소의 조건이 하나라도 만족하지 않으면 sql 문에 추가되지 않는다. 또한 위의 예에서는 하위 요소로 isNotNull 태그에 prepend=“and” 가 지정되어 있지만 처음 true 가 되는 조건의 prepend 는 parent 인 dynamic 의 prepend 인 “where” 로 덮어써져 최종적으로는 where EMP_NO = #empNo# 가 됨에 유의한다.
..
@Test
public void testDynamicStatement() throws Exception {
JobHistVO vo = new JobHistVO();
// 입력 파라메터 객체의 property 에 따른 Dynamic 테스트
vo.setEmpNo(new BigDecimal(7788));
// select
List<JobHistVO> resultList =
jobHistDAO.selectJobHistList(
"selectJobHistListUsingDynamicElement", vo);
// check
assertNotNull(resultList);
assertEquals(3, resultList.size());
SimpleDateFormat sdf =
new SimpleDateFormat("yyyy-MM-dd", java.util.Locale.getDefault());
assertEquals(sdf.parse("1987-04-19"), resultList.get(0).getStartDate());
assertEquals(sdf.parse("1988-04-13"), resultList.get(1).getStartDate());
assertEquals(sdf.parse("1990-05-05"), resultList.get(2).getStartDate());
// 입력 파라메터 객체의 property 에 따른 Dynamic 테스트
vo.setEmpNo(null);
// select
resultList =
jobHistDAO.selectJobHistList(
"selectJobHistListUsingDynamicElement", vo);
// check
assertNotNull(resultList);
// where 이 수행되지 않아 전체 데이터가 조회될 것임
assertEquals(17, resultList.size());
}
위에서 파라메터 객체에 empNo 의 값을 세팅했을 때 결과로는 해당 조건절이 동적으로 추가된 조회 결과인 사원번호 7788 에 해당하는 Job 이력 3건만 조회되지만 empNo 의 값을 세팅하지 않았을 때(위에서 null 로 세팅하는 것도 동일) 조회 조건절 없이 전체 사원에 대한 이력이 모두 조회됨을 확인할 수 있다.
아래의 샘플 sql mapping xml 예를 참고하라.
일반적인 경우 Dynamic SQL 작성을 위한 동적 요소는 Where 조건절의 변경을 위해 많이 쓰이지만, 아래에서는 테스트 편의의 목적으로 dual 에 대하여 임의의 상수를 입력 값으로 전달한 결과를 재조회 하는 과정에 Unary 비교 연산을 활용한 예이다.
..
<typeAlias alias="egovMap" type="egovframework.rte.psl.dataaccess.util.EgovMap" />
<select id="selectDynamicUnary" parameterClass="map" remapResults="true" resultClass="egovMap">
select
<dynamic>
<isEmpty property="testEmptyString">
'empty String' as IS_EMPTY_STRING
</isEmpty>
<isNotEmpty property="testEmptyString">
'not empty String' as IS_EMPTY_STRING
</isNotEmpty>
<isEmpty prepend=", " property="testEmptyCollection">
'empty Collection' as IS_EMPTY_COLLECTION
</isEmpty>
<isNotEmpty prepend=", " property="testEmptyCollection">
'not empty Collection' as IS_EMPTY_COLLECTION
</isNotEmpty>
<isNull prepend=", " property="testNull">
'null' as IS_NULL
</isNull>
<isNotNull prepend=", " property="testNull">
'not null' as IS_NULL
</isNotNull>
<isPropertyAvailable prepend=", " property="testProperty">
'testProperty Available' as TEST_PROPERTY_AVAILABLE
</isPropertyAvailable>
<isNotPropertyAvailable prepend=", " property="testProperty">
'testProperty Not Available' as TEST_PROPERTY_AVAILABLE
</isNotPropertyAvailable>
</dynamic>
from dual
</select>
위에서 테스트한 Unary 비교 연산 태그는 다음과 같다.
Unary 비교 연산 태그에 사용할 수 있는 속성은 다음과 같다.
..
@SuppressWarnings("unchecked")
@Test
public void testDynamicUnary() throws Exception {
Map map = new HashMap();
// 입력 파라메터 객체의 property 에 따른 Dynamic 테스트
// isEmpty 테스트 - String
map.put("testEmptyString", "");
// isEmpty 테스트 - Collection
List list = new ArrayList();
map.put("testEmptyCollection", list);
// isNull 테스트
map.put("testNull", null);
// isPropertyAvailable 테스트 - cf.) property 의 값을 null 로 설정하더라도 해당 property 는 Available 한것에 유의!
map.put("testProperty", null);
// select
Map resultMap =
(Map) jobHistDAO.getSqlMapClientTemplate().queryForObject(
"selectDynamicUnary", map);
// check
assertNotNull(resultMap);
assertEquals("empty String", resultMap.get("isEmptyString"));
assertEquals("empty Collection", resultMap.get("isEmptyCollection"));
assertEquals("null", resultMap.get("isNull"));
assertEquals("testProperty Available", resultMap
.get("testPropertyAvailable"));
// 입력 파라메터 객체의 property 에 따른 Dynamic 테스트 2
// isEmpty 테스트 - String - null 인 경우도 isEmpty 는 만족함
map.put("testEmptyString", null);
// isEmpty 테스트 - Collection - null 인 경우도 isEmpty 는 만족함
List nullList = null;
map.put("testEmptyCollection", nullList);
// select
resultMap =
(Map) jobHistDAO.getSqlMapClientTemplate().queryForObject(
"selectDynamicUnary", map);
// check
assertNotNull(resultMap);
assertEquals("empty String", resultMap.get("isEmptyString"));
assertEquals("empty Collection", resultMap.get("isEmptyCollection"));
// 입력 파라메터 객체의 property 에 따른 Dynamic 테스트 3
map.clear();
// isEmpty 테스트 - String
map.put("testEmptyString", "aa");
// isEmpty 테스트 - Collection
list.clear();
list.add("aa");
map.put("testEmptyCollection", list);
// isNull 테스트
map.put("testNull", new BigDecimal(0));
// isPropertyAvailable 테스트 - key 자체를 담지 않았을 때 isNotPropertyAvailable 임
// map.put("testProperty", null);
// select
resultMap =
(Map) jobHistDAO.getSqlMapClientTemplate().queryForObject(
"selectDynamicUnary", map);
// check
assertNotNull(resultMap);
assertEquals("not empty String", resultMap.get("isEmptyString"));
assertEquals("not empty Collection", resultMap.get("isEmptyCollection"));
assertEquals("not null", resultMap.get("isNull"));
assertEquals("testProperty Not Available", resultMap
.get("testPropertyAvailable"));
}
위에서 Unary 조건 비교를 위한 입력 파라메터 객체(여기서는 Map 사용)를 다양하게 세팅하여 어떤 경우에 어떤 조건이 만족하는지 테스트한 예이다. 위에서 isEmpty 의 경우 String 이 null 이거나 ””, Collection 에 하위 element 가 add 되지 않은 경우나 Collection 객체 자체가 null 인 경우에 모두 만족하는 것을 확인할 수 있으며, isPropertyAvailable 태그는 입력 객체에 해당 key 만 추가되있고 값은 null 인 경우에도 true 임을 확인할 수 있다. Dynamic SQL 의 동적인 where 조건절 변경의 경우 전달된 인자의 특정 property 에 대한 isNotNull 또는 isNotEmpty 로 간단히 비교하는 경우가 가장 많이 쓰이게 된다.
아래의 샘플 sql mapping xml 예를 참고하라.
마찬가지로 아래에서는 테스트 편의의 목적으로 dual 에 대하여 임의의 상수를 입력 값으로 전달한 결과를 재조회 하는 과정에 Binary 비교 연산을 활용한 예이다.
..
<typeAlias alias="egovMap" type="egovframework.rte.psl.dataaccess.util.EgovMap" />
<select id="selectDynamicBinary" parameterClass="map" remapResults="true" resultClass="egovMap">
select
<dynamic>
<isEqual property="testString" compareValue="test">
'$testString$' as TEST_STRING, 'test : equals' as IS_EQUAL
</isEqual>
<isNotEqual property="testString" compareValue="test">
'$testString$' as TEST_STRING, 'test : not equals' as IS_EQUAL
</isNotEqual>
<isPropertyAvailable property="testNumeric">
<isEqual property="testNumeric" prepend=", " compareValue="10">
cast($testNumeric$ as $castTypeScale$) as TEST_NUMERIC, '10 : equals' as IS_EQUAL_NUMERIC
</isEqual>
<isNotEqual property="testNumeric" prepend=", " compareValue="10">
cast($testNumeric$ as $castTypeScale$) as TEST_NUMERIC, '10 : not equals' as IS_EQUAL_NUMERIC
</isNotEqual>
</isPropertyAvailable>
<isGreaterEqual property="testNumeric" prepend=", " compareValue="10">
'10 <![CDATA[<=]]> $testNumeric$' as IS_GREATER_EQUAL
</isGreaterEqual>
<isGreaterThan property="testNumeric" prepend=", " compareValue="10">
'10 <![CDATA[<]]> $testNumeric$' as IS_GREATER_THAN
</isGreaterThan>
<isLessEqual property="testNumeric" prepend=", " compareValue="10">
'10 <![CDATA[>=]]> $testNumeric$' as IS_LESS_EQUAL
</isLessEqual>
<isLessThan property="testNumeric" prepend=", " compareValue="10">
'10 <![CDATA[>]]> $testNumeric$' as IS_LESS_THAN
</isLessThan>
<!-- checkMore -->
<isPropertyAvailable property="testOtherString">
<isEqual property="testOtherString" prepend=", " compareProperty="testString">
'$testOtherString$' as TEST_OTHER_STRING, 'test : testOtherString equals testString' as COMPARE_PROPERTY_EQUAL
</isEqual>
<isNotEqual property="testOtherString" prepend=", " compareProperty="testString">
'$testOtherString$' as TEST_OTHER_STRING, 'test : testOtherString not equals testString' as COMPARE_PROPERTY_EQUAL
</isNotEqual>
<isGreaterEqual property="testOtherString" prepend=", " compareProperty="testString">
'''$testOtherString$'' <![CDATA[>=]]> ''$testString$''' as COMPARE_PROPERTY_GREATER_EQUAL
</isGreaterEqual>
<isGreaterThan property="testOtherString" prepend=", " compareProperty="testString">
'''$testOtherString$'' <![CDATA[>]]> ''$testString$''' as COMPARE_PROPERTY_GREATER_THAN
</isGreaterThan>
<isLessEqual property="testOtherString" prepend=", " compareProperty="testString">
'''$testOtherString$'' <![CDATA[<=]]> ''$testString$''' as COMPARE_PROPERTY_LESS_EQUAL
</isLessEqual>
<isLessThan property="testOtherString" prepend=", " compareProperty="testString">
'''$testOtherString$'' <![CDATA[<]]> ''$testString$''' as COMPARE_PROPERTY_LESS_THAN
</isLessThan>
</isPropertyAvailable>
</dynamic>
from dual
</select>
위에서 테스트한 Binary 비교 연산 태그는 다음과 같다.
Binary 비교 연산 태그에 사용할 수 있는 속성은 다음과 같다.
위에서 각 비교 연산 태그의 중첩이 가능함을 확인할 수 있다. 복잡한 조건 처리가 필요한 경우 다양한 비교 연산 태그의 중첩으로 적절히 구성할 수 있을 것이다. 또한 비교 연산자 (>, <) 등과 같이 XML 에서 escape 처리가 필요한 경우 <![CDATA[>]]> 와 같이 CDATA 섹션으로 묶어 사용할 수 있다. CDATA 섹션을 전체 쿼리 영역에 묶어 한번에 사용하면 편하겠지만 Dynamic 요소 자체는 실제 XML 태그로 해석이 되어야 하므로 위와 같이 Dynamic 영역 내에서 발생하는 특수문자에 대해 개별로 사용하는 번거로움이 존재함에 유의한다. cf.) < , > 대신 < , > 와 같이 직접 escape 처리 할수도 있다.
..
@SuppressWarnings("unchecked")
@Test
public void testDynamicBinary() throws Exception {
Map map = new HashMap();
String castTypeScale = "numeric(2)";
// oracle 인 경우 - numeric 에 대응되는 type 은 number
if (isOracle) {
castTypeScale = "number(2)";
} else if (isMysql) {
castTypeScale = "decimal(2)";
}
// 입력 파라메터 객체의 property 에 따른 Dynamic 테스트
// isEqual 테스트 - String
map.put("testString", "test");
// isEqual 테스트 - BigDecimal
map.put("testNumeric", new BigDecimal(10));
// dual 임시 테이블 상에 상수 조회시 numeric(db) - decimal(java) 처리를 위해 cast 처리 추가
map.put("castTypeScale", castTypeScale);
// select
Map resultMap =
(Map) jobHistDAO.getSqlMapClientTemplate().queryForObject(
"selectDynamicBinary", map);
// check
assertNotNull(resultMap);
assertEquals("test", resultMap.get("testString"));
assertEquals("test : equals", resultMap.get("isEqual"));
assertEquals(new BigDecimal(10), resultMap.get("testNumeric"));
assertEquals("10 : equals", resultMap.get("isEqualNumeric"));
assertEquals("10 <= 10", resultMap.get("isGreaterEqual"));
assertTrue(!resultMap.containsKey("isGreaterThan"));
assertEquals("10 >= 10", resultMap.get("isLessEqual"));
assertTrue(!resultMap.containsKey("isLessThan"));
// 입력 파라메터 객체의 property 에 따른 Dynamic 테스트 2
map.clear();
// isEqual 테스트 - String
map.put("testString", "not test");
// isEqual 테스트 - BigDecimal
map.put("testNumeric", new BigDecimal(11));
// dual 임시 테이블 상에 상수 조회시 numeric(db) - decimal(java) 처리를 위해 cast 처리 추가
map.put("castTypeScale", castTypeScale);
// select
resultMap =
(Map) jobHistDAO.getSqlMapClientTemplate().queryForObject(
"selectDynamicBinary", map);
// check
assertNotNull(resultMap);
assertEquals("not test", resultMap.get("testString"));
assertEquals("test : not equals", resultMap.get("isEqual"));
assertEquals(new BigDecimal(11), resultMap.get("testNumeric"));
assertEquals("10 : not equals", resultMap.get("isEqualNumeric"));
assertEquals("10 <= 11", resultMap.get("isGreaterEqual"));
assertEquals("10 < 11", resultMap.get("isGreaterThan"));
assertTrue(!resultMap.containsKey("isLessEqual"));
assertTrue(!resultMap.containsKey("isLessThan"));
// 입력 파라메터 객체의 property 에 따른 Dynamic 테스트 2
map.clear();
// isEqual 테스트 - String
// isEqual 비교 대상 property 에 null 값을 넘기면 에러는 발생하지 않고, isNotEqual 과 매칭됨
map.put("testString", null);
// isEqual 테스트 - BigDecimal
map.put("testNumeric", new BigDecimal(9));
// dual 임시 테이블 상에 상수 조회시 numeric(db) - decimal(java) 처리를 위해 cast 처리 추가
map.put("castTypeScale", castTypeScale);
// select
resultMap =
(Map) jobHistDAO.getSqlMapClientTemplate().queryForObject(
"selectDynamicBinary", map);
// check
assertNotNull(resultMap);
// oracle 인 경우 '' 는 null 과 같고 결과 객체에는 null 로 맵핑됨
assertEquals(!(isOracle || isTibero) ? "" : null, resultMap
.get("testString"));
assertEquals("test : not equals", resultMap.get("isEqual"));
assertEquals(new BigDecimal(9), resultMap.get("testNumeric"));
assertEquals("10 : not equals", resultMap.get("isEqualNumeric"));
assertTrue(!resultMap.containsKey("isGreaterEqual"));
assertTrue(!resultMap.containsKey("isGreaterThan"));
assertEquals("10 >= 9", resultMap.get("isLessEqual"));
assertEquals("10 > 9", resultMap.get("isLessThan"));
// 입력 파라메터 객체의 property 에 따른 Dynamic 테스트 3
map.clear();
map.put("testString", "test");
// isEqual 테스트 - BigDecimal
map.put("testOtherString", "test");
// select
resultMap =
(Map) jobHistDAO.getSqlMapClientTemplate().queryForObject(
"selectDynamicBinary", map);
// check
assertNotNull(resultMap);
assertEquals("test : equals", resultMap.get("isEqual"));
// testNumeric property 를 넘기지 않았을 때 기대 결과
assertTrue(!resultMap.containsKey("isGreaterEqual"));
assertTrue(!resultMap.containsKey("isGreaterThan"));
assertTrue(!resultMap.containsKey("isLessEqual"));
assertTrue(!resultMap.containsKey("isLessThan"));
// testOtherString 비교
assertEquals("test", resultMap.get("testOtherString"));
assertEquals("test : testOtherString equals testString", resultMap
.get("comparePropertyEqual"));
assertEquals("'test' >= 'test'", resultMap
.get("comparePropertyGreaterEqual"));
assertTrue(!resultMap.containsKey("comparePropertyGreaterThan"));
assertEquals("'test' <= 'test'", resultMap
.get("comparePropertyLessEqual"));
assertTrue(!resultMap.containsKey("comparePropertyLessThan"));
// 입력 파라메터 객체의 property 에 따른 Dynamic 테스트 4
map.clear();
map.put("testString", "test");
// 'test' >= 'sample' 테스트
map.put("testOtherString", "sample");
// select
resultMap =
(Map) jobHistDAO.getSqlMapClientTemplate().queryForObject(
"selectDynamicBinary", map);
// check
assertNotNull(resultMap);
assertEquals("test : equals", resultMap.get("isEqual"));
// testNumeric property 를 넘기지 않았을 때 기대 결과
assertTrue(!resultMap.containsKey("isGreaterEqual"));
assertTrue(!resultMap.containsKey("isGreaterThan"));
assertTrue(!resultMap.containsKey("isLessEqual"));
assertTrue(!resultMap.containsKey("isLessThan"));
// testOtherString 비교
assertEquals("sample", resultMap.get("testOtherString"));
assertEquals("test : testOtherString not equals testString", resultMap
.get("comparePropertyEqual"));
assertTrue(!resultMap.containsKey("comparePropertyGreaterEqual"));
assertTrue(!resultMap.containsKey("comparePropertyGreaterThan"));
assertEquals("'sample' <= 'test'", resultMap
.get("comparePropertyLessEqual"));
assertEquals("'sample' < 'test'", resultMap
.get("comparePropertyLessThan"));
// 입력 파라메터 객체의 property 에 따른 Dynamic 테스트 5
map.clear();
map.put("testString", "test");
// 'test' <= 'testa' 테스트
map.put("testOtherString", "testa");
// select
resultMap =
(Map) jobHistDAO.getSqlMapClientTemplate().queryForObject(
"selectDynamicBinary", map);
// check
assertNotNull(resultMap);
assertEquals("test : equals", resultMap.get("isEqual"));
// testNumeric property 를 넘기지 않았을 때 기대 결과
assertTrue(!resultMap.containsKey("isGreaterEqual"));
assertTrue(!resultMap.containsKey("isGreaterThan"));
assertTrue(!resultMap.containsKey("isLessEqual"));
assertTrue(!resultMap.containsKey("isLessThan"));
// testOtherString 비교
assertEquals("testa", resultMap.get("testOtherString"));
assertEquals("test : testOtherString not equals testString", resultMap
.get("comparePropertyEqual"));
assertEquals("'testa' >= 'test'", resultMap
.get("comparePropertyGreaterEqual"));
assertEquals("'testa' > 'test'", resultMap
.get("comparePropertyGreaterThan"));
assertTrue(!resultMap.containsKey("comparePropertyLessEqual"));
assertTrue(!resultMap.containsKey("comparePropertyLessThan"));
}
위에서 Binary 조건 비교를 위한 입력 파라메터 객체(여기서는 Map 사용)를 다양하게 세팅하여 어떤 경우에 어떤 조건이 만족하는지 테스트한 예이다. 위에서 숫자형의 입력객체 속성에 대해 isGreaterEqual, isGreaterThan, isLessEqual, isLessThan 비교의 경우 쉽게 결과를 예상할 수 있으며 숫자 형식의 결과 조회를 위해 DB 단의 cast 를 처리하게 하였다. 테스트 시나리오 4, 5 에서 String 에 대한 isGreaterEqual, isGreaterThan, isLessEqual, isLessThan 가 가능함을 확인할 수 있으며 ‘sample’ < ’test’ , ’testa’ > ’test’ 임을 확인할 수 있다.
아래의 샘플 sql mapping xml 예를 참고하라.
..
<typeAlias alias="egovMap" type="egovframework.rte.psl.dataaccess.util.EgovMap" />
<select id="selectDynamicParameterPresent" parameterClass="map" remapResults="true" resultClass="egovMap">
select
<isParameterPresent>
'parameter object exist' as IS_PARAMETER_PRESENT
</isParameterPresent>
<isNotParameterPresent>
'parameter object not exist' as IS_PARAMETER_PRESENT
</isNotParameterPresent>
from dual
</select>
ParameterPresent 비교 연산 태그에 사용할 수 있는 속성은 다음과 같다.
..
@SuppressWarnings("unchecked")
@Test
public void testDynamicParameterPresent() throws Exception {
// 입력 파라메터 객체의 전달 여부에 따른 Dynamic 테스트
// isParameterPresent 테스트
Map map = new HashMap();
// select
Map resultMap =
(Map) jobHistDAO.getSqlMapClientTemplate().queryForObject(
"selectDynamicParameterPresent", map);
// check
assertNotNull(resultMap);
assertEquals("parameter object exist", resultMap
.get("isParameterPresent"));
map = null;
// select
resultMap =
(Map) jobHistDAO.getSqlMapClientTemplate().queryForObject(
"selectDynamicParameterPresent", map);
// check
assertNotNull(resultMap);
assertEquals("parameter object not exist", resultMap
.get("isParameterPresent"));
}
iBATIS 의 쿼리문 실행을 위한 API 호출 시 파라메터 객체의 전달 여부에 따라 isParameterPresent, isNotParameterPresent 의 비교 연산을 사용할 수 있다.
아래의 샘플 sql mapping xml 예를 참고하라.
일반적으로 iterate 태그 처리에 가장 많이 사용되는 in 조건절 처리 예이다.
..
<typeAlias alias="jobHistVO" type="egovframework.rte.psl.dataaccess.vo.JobHistVO" />
<typeAlias alias="empIncludesEmpListVO" type="egovframework.rte.psl.dataaccess.vo.EmpIncludesEmpListVO" />
<select id="selectJobHistListUsingDynamicIterate" parameterClass="empIncludesEmpListVO" resultClass="jobHistVO">
<![CDATA[
select EMP_NO as empNo,
START_DATE as startDate,
END_DATE as endDate,
JOB as job,
SAL as sal,
COMM as comm,
DEPT_NO as deptNo
from JOBHIST
]]>
<dynamic prepend="where">
<iterate property="empList" open="EMP_NO in (" conjunction=", " close=")">
#empList[].empNo#
</iterate>
</dynamic>
order by EMP_NO, START_DATE
</select>
iterate 태그에 사용할 수 있는 속성은 다음과 같다.
위에서는 empList 라는 attribute 로 List<EmpVO> 인 리스트 객체를 포함하는 EmpIncludesEmpListVO 가 파라메터 객체로 사용되고 있으며, iterate 태그에 의해서 empList 의 size 만큼 각 EmpVO 객체의 empNo 값이 in 리스트에 포함되는 아래의 조건절이 동적으로 만들어지게 된다.
where EMP_NO in ( ? , ? , ? )
iterate 태그의 body 영역 표기법에 유의한다. #empList[].empNo# 에서 확인할 수 있듯이 looping 중의 현재 item 을 지시하기 위해 ‘리스트속성명[]’ 이 사용되고 있으며 여기서는 해당 item 이 EmpVO 이고 empNo 라는 property 를 포함하고 있으며 이 값이 in 절의 현재 항목으로 바인딩되고 있다. 만약 파라메터 객체 자체가 iterate 가능한 형태인 경우 iterate property 의 명시 없이 body 영역에 ‘[]’ 로 바로 현재 item 을 지시할 수 있다.
..
@SuppressWarnings("unchecked")
@Test
public void testDynamicIterate() throws Exception {
// CompositeKeyTest.testCompositeKeySelect() 참조
EmpVO vo = new EmpVO();
// 7521,'WARD','SALESMAN',7698,'1981-02-22',1250,500,30
// --> mgr 이 7698 인 EMP
// 7499,'ALLEN','SALESMAN',7698,'1981-02-20',1600 --> O
// 7654,'MARTIN','SALESMAN',7698,'1981-09-28',1250 --> O
// 7844,'TURNER','SALESMAN',7698,'1981-09-08',1500 --> O
// 7900,'JAMES','CLERK',7698,'1981-12-03',950 --> X
vo.setEmpNo(new BigDecimal(7521));
// select
EmpIncludesEmpListVO resultVO =
empDAO.selectEmpIncludesEmpList(
"selectEmpIncludesSameMgrMoreSalaryEmpList", vo);
// check
assertNotNull(resultVO);
assertEquals(new BigDecimal(7521), resultVO.getEmpNo());
assertEquals("WARD", resultVO.getEmpName());
assertTrue(resultVO.getEmpList() instanceof List);
assertEquals(3, resultVO.getEmpList().size());
assertEquals(new BigDecimal(7499), resultVO.getEmpList().get(0)
.getEmpNo());
assertEquals(new BigDecimal(1600), resultVO.getEmpList().get(0)
.getSal());
assertEquals(new BigDecimal(7844), resultVO.getEmpList().get(1)
.getEmpNo());
assertEquals(new BigDecimal(1500), resultVO.getEmpList().get(1)
.getSal());
assertEquals(new BigDecimal(7654), resultVO.getEmpList().get(2)
.getEmpNo());
assertEquals(new BigDecimal(1250), resultVO.getEmpList().get(2)
.getSal());
// select
List<JobHistVO> resultList =
jobHistDAO.getSqlMapClientTemplate().queryForList(
"selectJobHistListUsingDynamicIterate", resultVO);
assertNotNull(resultList);
// 7499, 7654, 7844 의 jobhist 는 초기데이터에 따라 각 1건 임
assertEquals(3, resultList.size());
assertEquals(new BigDecimal(7499), resultList.get(0).getEmpNo());
assertEquals(new BigDecimal(7654), resultList.get(1).getEmpNo());
assertEquals(new BigDecimal(7844), resultList.get(2).getEmpNo());
SimpleDateFormat sdf =
new SimpleDateFormat("yyyy-MM-dd", java.util.Locale.getDefault());
assertEquals(sdf.parse("1981-02-20"), resultList.get(0).getStartDate());
assertEquals(sdf.parse("1981-09-28"), resultList.get(1).getStartDate());
assertEquals(sdf.parse("1981-09-08"), resultList.get(2).getStartDate());
}
위에서 이전에 작성한 CompositeKeyTest 의 쿼리를 실행하여 리스트 형태의 속성을 가지는 복합 객체를 만들었고, 이를 파라메터 객체로 iterate 테스트를 위한 쿼리를 수행하였다.
..
<!-- parameterClass 는 명시하지 않았음. Map 에 collection 이란 key 로 List 를 넘긴 경우와 바로 List를 넘긴 경우로 구분하여 테스트 -->
<!-- iterate 요소가 검색조건 등의 입력 파라메터 바인딩 변수로 사용될 경우는 #collection[]# 과 같이 사용하면 됨 -->
<select id="selectDynamicIterateSimple" resultClass="egovMap">
select
<isPropertyAvailable property="collection">
<iterate property="collection" conjunction=", ">
'$collection[]$' as $collection[]$
</iterate>
</isPropertyAvailable>
<!-- List 를 바로 넘긴 경우 -->
<isNotPropertyAvailable property="collection">
<iterate conjunction=", ">
'$[]$' as $[]$
</iterate>
</isNotPropertyAvailable>
from dual
</select>
collection 이란 속성이 포함됬는지 여부에 따라 iterate 대상이 파라메터 객체의 속성(위에서는 파라메터 객체 내에 collection 이라는 property 로 전달된 리스트) 또는 파라메터 객체 자신(파라메터 객체 자신이 리스트 형태인 경우) 에 대한 iterate 처리 예이다. $property명$ 로 작성된 영역은 #property명# 와 같이 prepared statement 의 바인드 변수로 처리되는 것이 아니라 SQL 문 자체에 텍스트가 replace 되어 처리됨에 유의한다.
..
@SuppressWarnings("unchecked")
@Test
public void testDynamicIterateSimple() throws Exception {
// Collection 형의 객체 size 만큼
List iterateList = new ArrayList();
iterateList.add("a");
iterateList.add("b");
iterateList.add("c");
// select
Map resultMap =
(Map) jobHistDAO.getSqlMapClientTemplate().queryForObject(
"selectDynamicIterateSimple", iterateList);
// check
assertNotNull(resultMap);
assertEquals("a", resultMap.get("a"));
assertEquals("b", resultMap.get("b"));
assertEquals("c", resultMap.get("c"));
assertTrue(!resultMap.containsKey("d"));
// map 안에 collection 이란 property 로 List 를 넣은 경우
Map map = new HashMap();
map.put("collection", iterateList);
// select
resultMap =
(Map) jobHistDAO.getSqlMapClientTemplate().queryForObject(
"selectDynamicIterateSimple", map);
// check
assertNotNull(resultMap);
assertEquals("a", resultMap.get("a"));
assertEquals("b", resultMap.get("b"));
assertEquals("c", resultMap.get("c"));
assertTrue(!resultMap.containsKey("d"));
// iterate 를 위한 테스트로 Map, Set, Iterator 를 시도해 보았으나 아래 에러를 냄. (List 나 Array 와 같이 index 로 접근 가능해야 하는듯)
// The 'xxx'(ex. collection) property of the XXX (ex. java.util.HashMap$EntryIterator) class is not a List or
// Array.
}
위에서 파라메터 객체 자체를 List 로 전달한 첫번째 경우와 파라메터 객체(Map) 안에 “collection” 이란 key 로 List 를 전달한 두번째 경우의 iterate 태그 처리 차이를 알 수 있을 것이다.
아래의 샘플 sql mapping xml 예를 참고하라.
..
<select id="selectJobHistListUsingDynamicNestedIterate" parameterClass="map" resultClass="jobHistVO">
<![CDATA[
select EMP_NO as empNo,
START_DATE as startDate,
END_DATE as endDate,
JOB as job,
SAL as sal,
COMM as comm,
DEPT_NO as deptNo
from JOBHIST
]]>
<dynamic prepend="where">
<iterate property="condition" open="(" conjunction="and" close=")">
$condition[].columnName$ $condition[].columnOperation$
<isEqual property="condition[].nested" compareValue="true">
<iterate property="condition[].columnValue" open="(" conjunction="," close=")">
#condition[].columnValue[]#
</iterate>
</isEqual>
<isNotEqual property="condition[].nested" compareValue="true">
#condition[].columnValue#
</isNotEqual>
</iterate>
</dynamic>
order by EMP_NO, START_DATE
</select>
복잡한 조건 처리의 예로 일반 비교 연산과 Nested Iterate 처리가 함께 사용되고 있다. 쿼리 호출 시 columnName, columnOperation, columnValue 를 멀티로 넘기며 columnName 과 columnOperation 은 sql 문에 직접 replaced Text 로 처리하고 columnValue 에 대해서는 바인드 변수 처리하며, 이때 nested 로 추가 설정한 값이 true 이면 columnValue 가 in 조건절인 경우로 판단하여 nested iterate 처리하는 예이다. 아래 테스트 케이스에서 파라메터 객체 세팅에 따라 다음 조건절이 동적으로 추가된다.
where ( DEPT_NO = ? and SAL < ? and JOB in ( ? , ? ) )
..
@SuppressWarnings("unchecked")
@Test
public void testDynamicNestedIterate() throws Exception {
// nested iterate 태그 테스트 - columnName, columnOperation, columnValue 를 Map 형태로 모아 담은 List 를 condition 이란 key 로 파라메터 객체(Map) 에 추가
// columnValue 가 nested iterate 로 풀려야 하는 경우(ex. in 조건절) nested 'true' 로 추가 설정을 하여 호출함.
Map map = new HashMap();
List condition = new ArrayList();
Map columnMap1 = new HashMap();
columnMap1.put("columnName", "DEPT_NO");
columnMap1.put("columnOperation", "=");
columnMap1.put("columnValue", new BigDecimal(30));
condition.add(columnMap1);
Map columnMap2 = new HashMap();
columnMap2.put("columnName", "SAL");
columnMap2.put("columnOperation", "<");
columnMap2.put("columnValue", new BigDecimal(3000));
condition.add(columnMap2);
Map columnMap3 = new HashMap();
columnMap3.put("columnName", "JOB");
columnMap3.put("columnOperation", "in");
List jobList = new ArrayList();
jobList.add("CLERK");
jobList.add("SALESMAN");
columnMap3.put("columnValue", jobList);
// List 를 nested 로 포함하고 있음을 flag 로 알림
columnMap3.put("nested", "true");
condition.add(columnMap3);
map.put("condition", condition);
// select
List<JobHistVO> resultList =
jobHistDAO.getSqlMapClientTemplate().queryForList(
"selectJobHistListUsingDynamicNestedIterate", map);
// check
assertNotNull(resultList);
// 결과 데이터
// Empno Startdate Enddate Job Sal Comm Deptno
// 1 7499 81/02/20 SALESMAN 1600 300 30
// 2 7521 81/02/22 SALESMAN 1250 500 30
// 3 7654 81/09/28 SALESMAN 1250 1400 30
// cf.) 7698 81/05/01 MANAGER 2850 30 데이터는 in 조건절에 JOB 이 'MANAGER' 인 것이 없기 때문에 nested 안에서 필터링 됨.
// 4 7844 81/09/08 SALESMAN 1500 0 30
// 5 7900 83/01/15 CLERK 950 30
assertEquals(5, resultList.size());
assertEquals(new BigDecimal(7499), resultList.get(0).getEmpNo());
assertEquals(new BigDecimal(7521), resultList.get(1).getEmpNo());
assertEquals(new BigDecimal(7654), resultList.get(2).getEmpNo());
assertEquals(new BigDecimal(7844), resultList.get(3).getEmpNo());
assertEquals(new BigDecimal(7900), resultList.get(4).getEmpNo());
SimpleDateFormat sdf =
new SimpleDateFormat("yyyy-MM-dd", java.util.Locale.getDefault());
assertEquals(sdf.parse("1981-02-20"), resultList.get(0).getStartDate());
assertEquals(sdf.parse("1981-02-22"), resultList.get(1).getStartDate());
assertEquals(sdf.parse("1981-09-28"), resultList.get(2).getStartDate());
assertEquals(sdf.parse("1981-09-08"), resultList.get(3).getStartDate());
assertEquals(sdf.parse("1983-01-15"), resultList.get(4).getStartDate());
}
지금까지 살펴본 바에서 확인할 수 있듯이 iBATIS 의 Dynamic 요소를 사용하여 매우 복잡한 조건 처리도 가능하다. 그러나 조건 처리가 복잡한 경우 dynamic 태그 영역을 쉽게 알아보기 어렵고 단순 논리/산술 연산 수준의 태그로 감당하기 어려운 복잡한 요구사항에 완벽하게 대응하기는 미비한 점이 존재한다. iBATIS 차후 버전에서는 좀더 유연하고 강력한 Dynamic 처리가 가능해질 걸로 보인다.
MyBatis
본 가이드는 MyBatis와 iBatis의 차이점을 설명한다.
| iBatis | MyBatis | 비고 |
|---|---|---|
| com.ibatis.* | org.apache.ibatis.* | 패키지 구조 변경 |
| SqlMapConfig | Configration | 용어변경 |
| sqlMap | mapper | 용어변경 |
| sqlMapClient | sqlSession | 구문대체 |
| rowHandler | resultHandler | 구문대체 |
| resultHandler | SqlSessionFactory | 구문대체 |
| parameterMap, parameterClass | parameterType | 속성 통합 |
| resultClass | resultType | 용어변경 |
| #var# | #{var} | 구문대체 |
| $var$ | ${var} | 구문대체 |
| 구문대체 |
| iBatis | MyBatis |
|---|---|
| com.ibatis.* | org.apache.ibatis.* |
패키지 구조는 변경되었으나 기존에 iBatis 패키지명은 그대로 사용한다.
Maven Dependency Information 예시
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.2.2</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>1.2.0</version>
</dependency>
| iBatis | MyBatis |
|---|---|
| <sqlMap namespace=“User”> | <mapper namespace=“myBatis.mapper.UserMapper”> |
실제 자바쪽에서 호출할 때도 길게 호출하여야 한다.
list = session.selectList("myBatis.mappers.UserMapper.getUserList");
이런 경우에 위에서 이야기한 자바 어노테이션 (@Select)을 사용해서 mapper 파일을 xml이 아니고 자바로 만들어 놓으면 코드 힌트까지 사용해서 편하게 쓸 수 있다.
UserMapper mapper = session.getMapper(UserMapper.class);
list = mapper.selectUserList();
기존에 조건에 따라 변하는 쿼리를 만들기 위해서 사용되던 태그들이 변경되었다. 조금 더 직관적으로 바뀌었고 해당상황(Update, Select)등에 맞춰서 사용할 수 있는 태그들도 추가되었다.
참고) #{var}와 ${var}의 차이는 prepredStatement의 파라미터로 사용, String 값으로 사용.
order by 같은 경우에 사용하기 위해서는 order by ${orderParam} 처럼 사용해야 한다.
이 방법을 사용하는 경우 MyBatis가 자체적으로 쿼리의 적합성여부를 판단할 수 없기 때문에 사용자의 입력 값을 그대로 사용하는 것보다는 개발자가 미리 정해 놓은 값 등으로 변경하도록 해서 정확한 값이 들어올 수 있도록 해야 한다.
sqlMap쪽에서 사용하던 typeAlias가 sqlMap이 바뀐 mapper 에서 사용되지 않고 Configration 파일에서 정의하도록 변경되었다.
<typeAliases>
<typeAlias type="vo.UserVO" alias="User"/>
</typeAliases>
Configuration 파일에 위의 형식처럼 Alias를 정의하면 전체 mapper 에서 사용할 수 있다.
<if test=“userID != null”> 형태로 간단하게 사용할 수 있다.
<dynamic> 형태로 해서 where 조건절이나 and , or 를 동적으로 만들던 것이 <where>나 update에서 사용할 수 있는 <set> 등으로 변경되었다.
<select id="getUserList" resultType="User>
SELECT * FROM TR_USER
<where>
<if test="isAdmin != null">
authLevel = '1'
</if>
</where>
</select>
모든 MyBatis 애플리케이션은 SqlSessionFactory 인스턴스를 사용한다. SqlSessionFactory 인스턴스는 SqlSessionFactoryBuilder 를 사용하여 만들 수 있다. SqlSessionFactoryBuilder 는 XML 설정파일에서 SqlSessionFactory 인스턴스를 빌드할 수 있다;
XML 파일에서 SqlSessionFactory 인스턴스를 빌드하는 것은 매우 간단하다. 설정을 위해 클래스패스 자원을 사용하는 것을 추천하나, 파일 경로나 file URL 로부터 만들어진 InputStream 인스턴스를 사용할 수도 있다. MyBatis 는 클래스패스와 다른 위치에서 자원을 로드하는 것으로 좀더 쉽게 해주는 Resources 라는 유틸성 클래스를 가지고 있다.
String resource = "org/mybatis/example/mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
XML 설정파일에서 지정하는 MyBatis 의 핵심이 되는 설정은 트랜잭션을 제어하기 위한 TransactionManager 과 함께 데이터베이스 Connection 인스턴스를 가져오기 위한 DataSource 를 포함한다. 예제 :
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="${driver}"/>
<property name="url" value="${url}"/>
<property name="username" value="${username}"/>
<property name="password" value="${password}"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="org/mybatis/example/BlogMapper.xml"/>
</mappers>
</configuration>
좀 더 많은 XML 설정방법이 있지만, 위 예제에서는 가장 핵심적인 부분만을 보여주고 있다. XML 가장 위부분에서는 XML 문서의 유효성체크를 위해 필요하다. environment 요소는 트랜잭션 관리와 커넥션 풀링을 위한 환경적인 설정을 나타낸다. mappers 요소는 SQL 코드와 매핑 정의를 가지는 XML 파일인 mapper 의 목록을 지정한다.
XML 보다 자바를 사용해서 직접 설정하길 원한다면, XML 파일과 같은 모든 설정을 제공하는 Configuration 클래스를 사용하면 된다.
DataSource dataSource = DeptDataSourceFactory.getDeptDataSource();
TransactionFactory transactionFactory = new JdbcTransactionFactory();
Environment environment = new Environment("development", transactionFactory, dataSource);
Configuration configuration = new Configuration(environment);
configuration.addMapper(DeptMapper.class);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(configuration);
이 설정에서 추가로 해야 할 일은 Mapper 클래스를 추가하는 것이다. Mapper 클래스는 SQL 매핑 어노테이션을 가진 자바 클래스이다. 어쨌든 자바 어노테이션의 몇 가지 제약과 몇 가지 설정방법의 복잡함에도 불구하고, XML 매핑은 세부적인 매핑을 위해 언제든 필요하다.
SqlSessionFactory 이름에서 보듯이, SqlSession 인스턴스를 만들 수 있다. SqlSession 은 데이터베이스에 대해 SQL 명령어를 실행하기 위해 필요한 모든 메서드를 가지고 있다. 그래서 SqlSession 인스턴스를 통해 직접 SQL 구문을 실행할 수 있다. 예를 들면 :
SqlSession session = sqlSessionFactory.openSession();
try {
Dept dept = session.selectOne("egovframework.rte.psl.dataaccess.DeptMapper.selectDept", 101);
} finally {
session.close();
}
이 방법이 MyBatis 의 이전버전을 사용한 사람이라면 굉장히 친숙할 것이다. 하지만 좀더 좋은 방법이 생겼다. 주어진 SQL 구문의 파라미터와 리턴값을 설명하는 인터페이스(예를 들면, DeptMapper.class )를 사용하여, 문자열 처리 오류나 타입 캐스팅 오류 없이 좀더 타입에 안전하고 깔끔하게 실행할 수 있다.
예를 들면:
SqlSession session = sqlSessionFactory.openSession();
try {
DeptMapper mapper = session.getMapper(DeptMapper.class);
Dept dept = mapper.selectDept(101);
} finally {
session.close();
}
그럼 좀더 자세히 살펴보자.
이 시점에 SqlSession 이나 Mapper 클래스가 정확히 어떻게 실행되는지 궁금할 것이다. 매핑된 SQL 구문에 대한 내용이 가장 중요하다. 그래서 이 문서 전반에서 가장 자주 다루어진다. 하지만 다음의 두가지 예제를 통해 정확히 어떻게 작동하는지에 대해서는 충분히 이해하게 될 것이다.
위 예제처럼, 구문은 XML 이나 어노테이션을 사용해서 정의할 수 있다. 그럼 먼저 XML 을 보자. MyBatis 가 제공하는 대부분의 기능은 XML 을 통해 매핑 기법을 사용한다. 이전에 MyBatis 를 사용했었다면 쉽게 이해되겠지만, XML 매핑 문서에 이전보다 많은 기능이 추가되었다. SqlSession 을 호출하는 XML 기반의 매핑 구문이다.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="egovframework.rte.psl.dataaccess.DeptMapper">
<select id="selectDept"
parameterType="int"
resultType="Dept">
<![CDATA[
select *
from DEPT
where DEPT_NO = #{deptNo}
]]>
</select>
</mapper>
한 개의 매퍼 XML 파일에는 많은 수의 매핑 구문을 정의할 수 있다. XML 도입부의 헤더와 doctype 을 제외하면, 나머지는 쉽게 이해되는 구문의 형태이다. 여기선 egovframework.rte.psl.dataaccess.DeptMapper 명명공간에서 selectDept 라는 매핑 구문을 정의했고, 이는 결과적으로 egovframework.rte.psl.dataaccess.DeptMapper.selectDept 형태로 실제 명시되게 된다. 그래서 다음처럼 사용하게 되는 셈이다.
Dept dept = (Dept) session.selectOne(“egovframework.rte.psl.dataaccess.DeptMapper.selectDept”, 101); 이건 마치 패키지를 포함한 전체 경로의 클래스내 메서드를 호출하는 것과 비슷한 형태이다. 이 이름은 매핑된 select 구문의 이름과 파라미터 그리고 리턴 타입을 가진 명명공간과 같은 이름의 Mapper 클래스와 직접 매핑될 수 있다. 이건 위에서 본 것과 같은 Mapper 인터페이스의 메서드를 간단히 호출하도록 허용한다. 위 예제에 대응되는 형태는 아래와 같다.
DeptMapper mapper = session.getMapper(DeptMapper.class); Dept dept = mapper.selectDept(101); 두번째 방법은 많은 장점을 가진다. 먼저 문자열에 의존하지 않는다는 것이다. 이는 애플리케이션을 좀더 안전하게 만든다. 두번째는 개발자가 IDE 를 사용할 때, 매핑된 SQL 구문을 사용할 때의 수고를 덜어준다. 세번째는 리턴 타입에 대해 타입 캐스팅을 하지 않아도 된다. 그래서 DeptMapper 인터페이스는 깔끔하고 리턴 타입에 대해 타입에 안전하며 이는 파라미터에도 그대로 적용된다.
명명공간(Namespaces) 이 이전 버전에서는 사실 선택 사항이었다. 하지만 이제는 패키지 경로를 포함한 전체 이름을 가진 구문을 구분하기 위해 필수로 사용해야 한다.
명명공간은 인터페이스 바인딩을 가능하게 한다. 명명공간을 사용하고 자바 패키지의 명명공간을 두면 코드가 깔끔해 지고 MyBatis 의 사용성이 크게 향상될 것이다.
이름 분석(Name Resolution): 타이핑을 줄이기 위해, MyBatis 는 구문과 결과맵, 캐시등의 모든 설정요소를 위한 이름 분석 규칙을 사용한다.
properties, settings, typeAliases, mappers 등 다양한 설정 항목으로 구성되며, 데이터베이스와의 상호작용을 정의하는 중요한 설정들을 포함한다. 이 파일은 MyBatis의 동작 방식과 데이터베이스 연결 환경을 관리하는 역할을 한다.MyBatis XML 설정파일은 다양한 셋팅과 프로퍼티를 가진다 해당 파일의 작성과 상세한 옵션 설정에 대해 알아본다.
MyBatis XML 설정파일의 일반적인 구조 및 구성은 properties, settings, typeAliases, typeHandlers, objectFactory, plugins, environments, databaseIdProvider, mappers 등의 내용으로 구성이 되어 있으며 주요 내용은 아래와 같다.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<properties resource="org/mybatis/example/config.properties">
<property name="username" value="dev_user"/>
<property name="password" value="F2Fa3!33TYyg"/>
</properties>
<settings>
<setting name="cacheEnabled" value="true"/>
<setting name="lazyLoadingEnabled" value="true"/>
<setting name="multipleResultSetsEnabled" value="true"/>
</settings>
<typeHandlers>
<typeHandler handler="egovframework.rte.psl.dataaccess.typehandler.CalendarMapperTypeHandler" />
</typeHandlers>
<typeAliases>
<typeAlias alias="deptVO" type="egovframework.rte.psl.dataaccess.vo.DeptVO" />
<typeAlias alias="empVO" type="egovframework.rte.psl.dataaccess.vo.EmpVO" />
.
.
.
</typeAliases>
.
.
.
.
</configuration>
properties : 표준 java properties (key=value 형태)파일에 대한 연결을 지원하며 설정 파일내에서 ${key} 와 같은 형태로 properties 형태로 외부화해놓은 실제 값(여기서는 DB 접속 관련 driver, url, id/pw)을 참조할 수 있다. resource 속성으로 classpath, url 속성으로 유효한 URL 상에 있는 자원을 지정 가능하다.
settings : 런타임시 MyBatis의 행위를 조정하기 위한 옵션 설정을 통해 최적화할 수 있도록 지원한다. 다음표는 셋팅과 그 의미 그리고 디폴트 값을 설명한다.
| 셋팅 | 설명 | 사용가능한 값들 | 디폴트 |
|---|---|---|---|
| cacheEnabled | 설정에서 각 mapper 에 설정된 캐시를 전역적으로 사용할지 말지에 대한 여부 | true / false | TRUE |
| lazyLoadingEnabled | 늦은 로딩을 사용할지에 대한 여부. 사용하지 않는다면 모두 즉시 로딩할 것이다. | true / false | TRUE |
| aggressiveLazyLoading | 활성화 상태로 두게 되면 늦은(lazy) 로딩 프로퍼티를 가진 객체는 호출에 따라 로드될 것이다. 반면에 개별 프로퍼티는 요청할때 로드된다. | true / false | TRUE |
| multipleResultSetsEnabled | 한개의 구문에서 여러개의 ResultSet 을 허용할지의 여부(드라이버가 해당 기능을 지원해야 함) | true / false | TRUE |
| useColumnLabel | 칼럼명 대신에 칼럼라벨을 사용. 드라이버마다 조금 다르게 작동한다. 문서와 간단한 테스트를 통해 실제 기대하는 것처럼 작동하는지 확인해야 한다. | true / false | TRUE |
| useGeneratedKeys | 생성키에 대한 JDBC 지원을 허용. 지원하는 드라이버가 필요하다. true 로 설정하면 생성키를 강제로 생성한다. 일부 드라이버(예를들면, Derby)에서는 이 설정을 무시한다. | true / false | FALSE |
| autoMappingBehavior | MyBatis 가 칼럼을 필드/프로퍼티에 자동으로 매핑할지와 방법에 대해 명시. PARTIAL 은 간단한 자동매핑만 할뿐, 내포된 결과에 대해서는 처리하지 않는다. FULL 은 처리가능한 모든 자동매핑을 처리한다. | NONE, PARTIAL, FULL | PARTIAL |
| defaultExecutorType | 디폴트 실행자(executor) 설정. SIMPLE 실행자는 특별히 하는 것이 없다. REUSE 실행자는 PreparedStatement 를 재사용한다. BATCH 실행자는 구문을 재사용하고 수정을 배치처리한다. | SIMPLE REUSE BATCH | SIMPLE |
| defaultStatementTimeout | 데이터베이스로의 응답을 얼마나 오래 기다릴지를 판단하는 타임아웃을 셋팅 | 양수 | 셋팅되지 않음(null) |
| safeRowBoundsEnabled | 중첩구문내 RowBound 사용을 허용 | true / false | FALSE |
| mapUnderscoreToCamelCase | 전통적인 데이터베이스 칼럼명 형태인 A_COLUMN을 CamelCase형태의 자바 프로퍼티명 형태인 aColumn으로 자동으로 매핑하도록 함 | true / false | FALSE |
| localCacheScope | MyBatis uses local cache to prevent circular references and speed up repeated nested queries. By default (SESSION) all queries executed during a session are cached. If localCacheScope=STATEMENT local session will be used just for statement execution, no data will be shared between two different calls to the same SqlSession. | SESSION / STATEMENT | SESSION |
| jdbcTypeForNull | Specifies the JDBC type for null values when no specific JDBC type was provided for the parameter. Some drivers require specifying the column JDBC type but others work with generic values like NULL, VARCHAR or OTHER. | JdbcType enumeration. Most common are: NULL, VARCHAR and OTHER | OTHER |
| lazyLoadTriggerMethods | Specifies which Object’s methods trigger a lazy load | A method name list separated by commas | equals,clone,hashCode,toString |
| defaultScriptingLanguage | Specifies the language used by default for dynamic SQL generation. | A type alias or fully qualified class name. | org.apache.ibatis.scripting.xmltags.XMLDynamicLanguageDriver |
| callSettersOnNulls | Specifies if setters or map’s put method will be called when a retrieved value is null. It is useful when you rely on Map.keySet() or null value initialization. Note primitives such as (int,boolean,etc.) will not be set to null. | true / false | FALSE |
| logPrefix | Specifies the prefix string that MyBatis will add to the logger names. | Any String | Not set |
| logImpl | Specifies which logging implementation MyBatis should use. If this setting is not present logging implementation will be autodiscovered. | SLF4J / LOG4J / LOG4J2 / JDK_LOGGING / COMMONS_LOGGING / STDOUT_LOGGING / NO_LOGGING | Not set |
| proxyFactory | Specifies the proxy tool that MyBatis will use for creating lazy loading capable objects. | CGLIB / JAVASSIST | CGLIB |
typeAliases : 타입 별칭을 통해 자바 타입에 대한 좀더 짧은 이름을 사용할 수 있다. 오직 XML 설정에서만 사용되며, 타이핑을 줄이기 위해 사용된다.
typeHandler : MyBatis 가 PreparedStatement 에 파라미터를 셋팅하고 ResultSet 에서 값을 가져올때마다, TypeHandler 는 적절한 자바 타입의 값을 가져오기 위해 사용된다. typeHandler 구현체를 등록하여 사용할 수 있다.
objectFactory : MyBatis 는 결과 객체의 인스턴스를 만들기 위해 ObjectFactory를 사용한다.
mappers: 매핑된 SQL 구문을 정의한다. 해당 매퍼 파일의 지정은 클래스패스에 상대적으로 리소스를 지정할 수도 있고, url 을 통해서 지정할 수 도 있다
이외에도 plugins (매핑 구문을 실행하는 어떤 시점에 호출을 가로챈다. 기본적으로 MyBatis 는 메서드 호출을 가로채기 위한 플러그인을 허용한다), environments (여러개의 환경으로 설정), databaseIdProvider(데이터베이스 제품마다 다른 구문을 실행) 등의 설정을 통해 다양한 환경 및 시점에서 추가적인 설정이 가능하다.
MyBatis Mapper XML (SQL Mapping XML) File은 실행할 SQL문을 정의해놓은 파일로서,
SQL문 실행을 위해 Parameter Object를 받아오거나 SQL문 실행 결과를 Result Object에 자동 바인딩하는 기능 등을 제공한다.
Mapper XML File에는 다음과 같은 요소들을 사용할 수 있다.
<select>: 매핑된 SELECT 구문<insert>: 매핑된 INSERT 구문<update>: 매핑된 UPDATE 구문<delete>: 매핑된 DELETE 구문<sql>: 다른 구문에서 재사용하기 위한 SQL 조각<resultMap>: 데이터베이스 결과 데이터를 객체에 매핑하는 방법을 정의<cache>: 자신의 namespace를 위한 캐시설정<cache-ref>: 다른 namespace의 캐시설정을 참조<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="egovframework.rte.psl.dataaccess.DeptMapper"> <!-- MyBatis에서는 namespace를 필수로 설정해야함 -->
<resultMap id="deptResult" type="egovframework.rte.psl.dataaccess.vo.DeptVO"> <!-- [주의] iBatis의 class속성 -> type속성으로 변경 -->
<result property="deptNo" column="DEPT_NO" />
<result property="deptName" column="DEPT_NAME" />
<result property="loc" column="LOC" />
</resultMap>
<select id="selectDept" parameterType="int" resultMap="deptResult"> <!-- [주의] iBatis의 parameterClass속성 -> parameterType속성으로 변경 -->
<![CDATA[
select DEPT_NO, DEPT_NAME, LOC
from DEPT
where DEPT_NO = #{deptNo}
]]>
</select>
<insert id="insertDept" parameterType="egovframework.rte.psl.dataaccess.vo.DeptVO">
<![CDATA[
insert into DEPT
(DEPT_NO, DEPT_NAME, LOC)
values (#{deptNo}, #{deptName}, #{loc})
]]>
</insert>
<update id="updateDept" parameterType="egovframework.rte.psl.dataaccess.vo.DeptVO">
<![CDATA[
update DEPT
set DEPT_NAME = #{deptName},
LOC = #{loc}
where DEPT_NO = #{deptNo}
]]>
</update>
<delete id="deleteDept" parameterType="egovframework.rte.psl.dataaccess.vo.DeptVO">
<![CDATA[
delete from DEPT
where DEPT_NO = #{deptNo}
]]>
</delete>
</mapper>
<select><select>는 SELECT문을 작성할 때 사용되는 요소로, 데이터베이스에서 조회한 결과를 오브젝트에 매핑하는 역할을 한다.
먼저 <select> 요소에서 사용 가능한 속성들에 대해 알아보고, 위 샘플코드에서 언급된 <select> 설정을 자세히 살펴보자.
| 속성 | 설명 |
|---|---|
id | 해당 구문을 호출할 때 사용되는 값으로, 각 SQL문을 구분하는 유일한 식별자 (필수) |
parameterType | 해당 구문에 전달될 파라미터의 패키지 경로를 포함한 전체 클래스명이나 별칭 |
parameterMap | <select> 외부에 정의된 Parameter Object(<parameterMap>)를 참조하는 방법, 권장하지 않음. parameterType 속성이나 Inline Parameter를 권장 |
resultType | 해당 구문이 리턴하는 타입의 패키지 경로를 포함한 전체 클래스명이나 별칭 |
resultMap | <select> 외부에 정의된 Result Object(<resultMap>)를 참조하는 방법 |
flushCache | 이 속성값이 true이면, 구문이 호출될 때마다 캐시가 지원됨 (default: false) |
useCache | 이 속성값이 true이면, 구문의 결과가 캐시됨 (default: true) |
timeout | 예외가 던져지기 전에 데이터베이스의 요청 결과를 기다리는 최대 시간 (드라이버에 따라 다소 지원되지 않을 수 있음) |
<select id="selectDept" parameterType="int" resultMap="deptResult">
<![CDATA[
select DEPT_NO,
DEPT_NAME,
LOC
from DEPT
where DEPT_NO = #{deptNo} <!-- 파라미터 바인딩 표기법 #{property} -->
]]>
</select>
위의 <select> 구문은 ‘selectDept’를 이용하여 호출하며, ‘int’ 타입의 파라미터를 받아와 WHERE절 조건식에 바인딩하고,
SELECT 결과를 ‘deptResult’라는 이름을 가진 <resultMap> 설정에 따라 오브젝트에 매핑하여 리턴한다.
<resultMap id="deptResult" ... /> 형태로 <select> 외부에 정의되어 있다.
<insert>, <update>, <delete><insert>, <update>, <delete>는 각각 INSERT, UPDATE, DELETE문을 작성할 때 사용되는 요소로, 필요한 파라미터를 전달받아 데이터베이스의 데이터를 변경하는 역할을 한다.
먼저 <insert>, <update>, <delete> 요소에서 사용 가능한 속성들에 대해 알아보고, 위 샘플코드에서 언급된 설정을 각각 살펴보겠습니다.
<insert>| 속성 | 설명 |
|---|---|
id | 해당 구문을 호출할 때 사용되는 값으로, 각 SQL문을 구분하는 유일한 식별자 (필수) |
parameterType | 해당 구문에 전달될 파라미터의 패키지 경로를 포함한 전체 클래스명이나 별칭 |
flushCache | 이 속성값이 true이면, 구문이 호출될 때마다 캐시가 지원됨 (default: false) |
timeout | 예외가 던져지기 전에 데이터베이스의 요청 결과를 기다리는 최대 시간 (드라이버에 따라 다소 지원되지 않을 수 있음) |
statementType | STATEMENT / PREPARED / CALLABLE 중 선택, MyBatis에게 Statement, PreparedStatement 또는 CallableStatement를 사용하게 함 (default: PREPARED) |
useGeneratedKeys | DB 내부에서 생성한 키를 받는 JDBC getGeneratedKeys 메서드를 사용하도록 설정 (default: false) |
keyProperty | getGeneratedKeys 메서드나 INSERT 구문의 selectKey 하위 요소에 의해 리턴된 키를 셋팅할 프로퍼티를 지정 |
keyColumn | 생성키를 가진 테이블의 칼럼명을 셋팅 (키 칼럼이 테이블의 첫번째 칼럼이 아닐 경우 필요) |
<insert id="insertDept" parameterType="egovframework.rte.psl.dataaccess.vo.DeptVO">
<![CDATA[
insert into DEPT (DEPT_NO, DEPT_NAME, LOC)
values (#{deptNo}, #{deptName}, #{loc}) <!-- 파라미터 바인딩 표기법 #{property} -->
]]>
</insert>
위의 <insert> 구문은 ‘insertDept’를 이용하여 호출하며, ’egovframework.rte.psl.dataaccess.vo.DeptVO’ 타입의 파라미터를 받아와 INSERT 절에 바인딩한다.
<update>| 속성 | 설명 |
|---|---|
id | 해당 구문을 호출할 때 사용되는 값으로, 각 SQL문을 구분하는 유일한 식별자 (필수) |
parameterType | 해당 구문에 전달될 파라미터의 패키지 경로를 포함한 전체 클래스명이나 별칭 |
flushCache | 이 속성값이 true이면, 구문이 호출될 때마다 캐시가 지원됨 (default: false) |
timeout | 예외가 던져지기 전에 데이터베이스의 요청 결과를 기다리는 최대 시간 (드라이버에 따라 다소 지원되지 않을 수 있음) |
statementType | STATEMENT / PREPARED / CALLABLE 중 선택, MyBatis에게 Statement, PreparedStatement 또는 CallableStatement를 사용하게 함 (default: PREPARED) |
<update id="updateDept" parameterType="egovframework.rte.psl.dataaccess.vo.DeptVO">
<![CDATA[
update DEPT
set DEPT_NAME = #{deptName}, <!-- 파라미터 바인딩 표기법 #{property} -->
LOC = #{loc}
where DEPT_NO = #{deptNo}
]]>
</update>
위의
<delete>| 속성 | 설명 |
|---|---|
id | 해당 구문을 호출할 때 사용되는 값으로, 각 SQL문을 구분하는 유일한 식별자 (필수) |
parameterType | 해당 구문에 전달될 파라미터의 패키지 경로를 포함한 전체 클래스명이나 별칭 |
flushCache | 이 속성값이 true이면, 구문이 호출될 때마다 캐시가 지원됨 (default: false) |
timeout | 예외가 던져지기 전에 데이터베이스의 요청 결과를 기다리는 최대 시간 (드라이버에 따라 다소 지원되지 않을 수 있음) |
statementType | STATEMENT / PREPARED / CALLABLE 중 선택, MyBatis에게 Statement, PreparedStatement 또는 CallableStatement를 사용하게 함 (default: PREPARED) |
<delete id="deleteDept" parameterType="egovframework.rte.psl.dataaccess.vo.DeptVO">
<![CDATA[
delete from DEPT
where DEPT_NO = #{deptNo} -- 파라미터 바인딩 표기법 #{property}
]]>
</delete>
위의 <delete> 구문은 deleteDept를 이용하여 호출하며, egovframework.rte.psl.dataaccess.vo.DeptVO 타입의 파라미터를 받아와 WHERE절에 바인딩한다.
parameterType에 지정한 전체 클래스명이 복잡하다면, 해당 클래스타입에 대한 Alias(별칭)로 대체할 수 있다.
<!-- In MyBatis Configuration XML File -->
<typeAlias type="egovframework.rte.psl.dataaccess.vo.DeptVO" alias="deptVO" />
<!-- Mapper XML File -->
<delete id="deleteDept" parameterType="deptVO">
<![CDATA[
delete from DEPT
where DEPT_NO = #{deptNo} -- 파라미터 바인딩 표기법 #{property}
]]>
</delete>
<sql><sql> 요소는 다른 구문에서 재사용 가능한 SQL구문을 정의할 때 사용한다.
<sql id="userColumns"> id, username, password </sql>
<select id="selectUsers" resultType="map">
<![CDATA[
select <include refid="userColumns"/>
from some_table
where id = #{id}
]]>
</select>
예제 코드에서 파라미터를 전달하는 간단한 구문을 살펴보도록 하겠다.
<insert id="insertDept" parameterType="egovframework.rte.psl.dataaccess.vo.DeptVO">
<![CDATA[
insert into DEPT
(DEPT_NO,
DEPT_NAME,
LOC)
values (#{deptNo},
#{deptName},
#{loc})
]]>
</insert>
위에서 egovframework.rte.psl.dataaccess.vo.DeptVO 클래스 타입의 객체가 mapper 오브젝트를 통해 전달될 경우 해당 객체의 deptNo, deptName, loc를 찾아서 PreparedStatement 파라미터로 전달된다.
추가적으로 파라미터 전달 시 파라미터에 다음과 같은 형태로 데이터 타입을 명시할 수 있다:
#{property, javaType=int, jdbcType=NUMERIC}
<resultMap><resultMap>는 SELECT 조회 결과값을 오브젝트에 매핑하기 위해 사용하는 요소로, ResultSet에서 데이터를 가져올 때 필요한 JDBC 코드를 대신한다.
| 속성 | 설명 |
|---|---|
| id | Statement에서 resultMap을 참조하기 위한 유일한 식별자 |
| type | 구문이 리턴한 결과를 매핑할 오브젝트 타입의 패키지 경로를 포함한 전체 클래스명이나 별칭 |
<!-- 기본 예제 코드 -->
<resultMap id="deptResult" type="egovframework.rte.psl.dataaccess.vo.DeptVO">
<result property="deptNo" column="DEPT_NO" />
<result property="deptName" column="DEPT_NAME" />
<result property="loc" column="LOC" />
</resultMap>
<select id="selectDept" parameterType="egovframework.rte.psl.dataaccess.vo.DeptVO" resultMap="deptResult">
<![CDATA[
select DEPT_NO,DEPT_NAME,LOC
from DEPT
where DEPT_NO = #{deptNo}
]]>
</select>
SELECT문 결과값은 <select>에 지정한 resultMap 속성값에 따라 <resultMap id="deptResult">에 매핑되어 리턴된다.
또는 다음과 같이 <select>에서 resultMap 속성 대신 resultType 속성을 사용하여 결과값을 매핑할 클래스 타입을 지정할 수도 있다.
단, 아래와 같이 resultType 속성을 이용하는 경우에는 DB 컬럼명과 프로퍼티명이 동일해야 한다.
<select id="selectDept" parameterType="egovframework.rte.psl.dataaccess.vo.DeptVO" resultType="egovframework.rte.psl.dataaccess.vo.DeptVO">
<![CDATA[
select deptno, deptname, loc
from DEPT
where deptno = #{deptno}
]]>
</select>
위의 두 코드는 SELECT문의 칼럼명과 Result Object(DeptVO)의 프로퍼티명을 비교하여 일치하는 프로퍼티의 setter를 찾아 결과값을 매핑한다.
<!-- 1) resultType 속성 사용 시 -->
<select id="selectDept" resultType="deptVO">
<![CDATA[
select
DEPT_NO as "deptNo",
DEPT_NAME as "deptName",
LOC as "loc"
from DEPT
where DEPT_NO = #{deptNo}
]]>
</select>
<!-- 2) <resultMap> 사용 시 -->
<resultMap id="deptResult" type="deptVO">
<result property="deptNo" column="DEPT_NO" />
<result property="deptName" column="DEPT_NAME" />
<result property="loc" column="LOC" />
</resultMap>
<select id="selectDept" parameterType="deptVO" resultMap="deptResult">
<![CDATA[
select DEPT_NO, DEPT_NAME, LOC
from DEPT
where DEPT_NO = #{deptNo}
]]>
</select>
1)번의 경우, 컬럼 Alias를 이용하여 프로퍼티명과 일치시킨다.
2)번의 경우,
[참고] ResultSet 처리를 위해 hashmap 등으로 키 형태로 매핑하여 결과를 받아올 수 있지만, 좋지 않은 방법이므로 resultMap의 도메인 모델로는 자바빈이나 POJO를 사용한다.
<cache>MyBatis는 쉽게 설정 가능하고 변경 가능한 쿼리 캐싱 기능을 가지고 있다. MyBatis 3 캐시 구현체는 좀 더 쉽게 설정할 수 있도록 많은 부분이 수정되었다.
성능을 개선하고 순환하는 의존성을 해결하기 위해 필요한 로컬 세션 캐싱을 제외하고 기본적으로 캐시가 작동하지 않는다. 캐싱을 활성화하기 위해서, SQL 매핑 파일에 다음 한 줄을 추가하면 된다.
<cache/><select> 구문의 결과는 캐시된다.<insert>, <update>, <delete> 구문이 호출될 때 캐시가 플러시된다.이 외에 캐시 설정에 대해서는 MyBatis 매뉴얼을 참고하시길 바랍니다.
<cache-ref>MyBatis 에서는 이전 섹션 내용을 돌이켜보면서, 특정 명명공간을 위한 캐시는 오직 하나만 사용되거나 같은 명명공간내에서는 구문마다 캐시를 지울수 있다.
명명공간간의 캐시 설정과 인스턴스를 공유하고자 할때가 있을 것이다.
이 경우 cache-ref 요소를 사용해서 다른 캐시를 참조할 수 있다.
<cache-ref namespace=”com.someone.application.data.SomeMapper” />일반적으로 JDBC API를 사용한 코딩에서 다양한 조건에 따라 다양한 형태의 쿼리의 실행이 필요한 경우가 존재하며 이에 MyBatis는 강력한 동적 SQL 언어를 제공한다.
MyBatis는 SQL 문의 동적인 변경에 대해 iBatis보다 상대적으로 유연하다.
iBatis도 다양한 Dynamic 요소를 제공하였으나 이해해야 하는 요소들이 많았다.
MyBatis에서 제공하는 동적 SQL 요소들은 JSTL이나 XML 기반의 텍스트 프로세서와 유사한 형태로 제공되며 OGNL 기반의 표현식을 제공함으로써 보다 유연하고 편리하게 Dynamic 요소를 사용할 수 있다.
MyBatis에서 제공하는 Dynamic 요소의 기본적인 형태에 대해 알아보도록 한다.
아래 Sql 매퍼 파일은 파라미터 객체의 empNo 속성의 값 유무에 따라 where EMP_NO = #{empNo} 조건절을 동적으로 추가/제거할 수 있는 예이다.
<select id="selectJobHistListUsingDynamicElement" parameterType="egovframework.rte.psl.dataaccess.vo.JobHistVO" resultMap="egovframework.rte.psl.dataaccess.vo.JobHistVO">
<![CDATA[
select EMP_NO as empNo,
START_DATE as startDate,
END_DATE as endDate,
JOB as job,
SAL as sal,
COMM as comm,
DEPT_NO as deptNo
from JOBHIST
]]>
<where>
<if test="empNo != null">
EMP_NO = #{empNo}
</if>
</where>
</select>
where 요소는 태그에 의해 콘텐츠가 리턴되면 단순히 “WHERE” 만을 추가한다. 하지만 하위 요소의 조건이 하나라도 만족하지 않으면 추가되지 않는다.
또한 콘텐츠가 “AND” 나 “OR”로 시작한다면, 그 “AND” 나 “OR”를 지워버리는 점에 유념하여 사용하도록 한다.
if는 가장 많이 사용되는 Dynamic 요소로 문자열을 선택적으로 검색하는 기능을 제공한다.
where 절 안에 사용되며 if 안에 해당 값이 존재하지 않으면 모든 결과값이 리턴된다.
..
<select id="selectEmployerList" parameterType="egovframework.rte.psl.dataaccess.vo.EmpVO" resultType="egovframework.rte.psl.dataaccess.vo.EmpVO">
<![CDATA[
select
EMP_NO as empNo,
EMP_NAME as empName,
JOB as job,
MGR as mgr,
HIRE_DATE as hireDate,
SAL as sal,
COMM as comm,
DEPT_NO as deptNo
from EMP
]]>
<where>
<if test="empNo != null">
EMP_NO = #{empNo}
</if>
<if test="empName != null">
EMP_NAME LIKE '%' || #{empName} || '%'
</if>
</where>
</select>
전달된 인자의 특정 property에 대해 if로 비교하는 경우가 가장 많이 사용된다.
모든 조건을 적용하는 대신 한 가지 조건 만을 적용해야 할 필요가 있는 경우 MyBatis에서 제공하는 choose 요소를 사용하며 이는 자바의 switch 구문과 유사한 개념이다.
아래 예제를 보면 지금은 MGR 정보만으로 검색하고, EMP 정보가 있다면 그 값으로 검색된다.
두 가지 값을 모두 제공하지 않는다면 HIRE 상태인 Employee 정보가 리턴될 것이다.
<select id="selectEmployeeList" parameterType="egovframework.rte.psl.dataaccess.vo.EmpVO" resultType="egovframework.rte.psl.dataaccess.vo.EmpVO">
SELECT * FROM EMP WHERE JOB = 'Engineer'
<choose>
<when test="mgr ! null">
AND MGR like #{mgr}
</when>
<when test="empNo ! null and empName ! =null">
AND EMP_NAME like #{empName}
</when>
<otherwise>
AND HIRE_STATUS = 'Y'
</otherwise>
</choose>
</select>
아래 예제의 <trim prefix=“WHERE” prefixOverrides=“AND|OR”>은 <where>와 동일하게 동작한다. 즉, where 요소에 대한 trim 기능을 제공한다.
..
<select id="selectEmployerList" parameterType="egovframework.rte.psl.dataaccess.vo.EmpVO"
resultType="egovframework.rte.psl.dataaccess.vo.EmpVO">
<![CDATA[
select
EMP_NO as empNo,
EMP_NAME as empName,
JOB as job,
MGR as mgr,
HIRE_DATE as hireDate,
SAL as sal,
COMM as comm,
DEPT_NO as deptNo
from EMP
]]>
<trim prefix="WHERE" prefixOverrides="AND|OR ">
<if test="empNo != null">
EMP_NO = #{empNo}
</if>
<if test="empName != null">
EMP_NAME LIKE '%' || #{empName} || '%'
</if>
</trim>
</select>
아래의 샘플 sql mapper xml 예를 참고하라. 일반적으로 iterate 태그 처리에 가장 많이 사용되는 in 조건절 처리 예이다.
foreach 요소는 매우 강력한 기능을 제공하는데 그중 하나가 collection을 명시하는 것을 허용하는 것이다.
foreach 요소에서는 item, index 두 가지 변수를 선언하며, 이 요소는 열고 닫는 문자열로 명시할 수 있고 반복 간에 둘 수 있는 구분자도 추가 가능하다.
<select id="selectJobHistListUsingDynamicNestedIterate" parameterType="egovframework.rte.psl.dataaccess.util.EgovMap" resultMap="jobHistVO">
<![CDATA[
select EMP_NO as empNo,
START_DATE as startDate,
END_DATE as endDate,
JOB as job,
SAL as sal,
COMM as comm,
DEPT_NO as deptNo
from JOBHIST
]]>
where
<foreach collection="condition" item="item" open="(" separator="and" close=")">
${item.columnName} ${item.columnOperation}
<if test="item.nested == 'true'">
<foreach item="item" index="index" collection="item.columnValue" open="(" separator="," close=")">
'${item}'
</foreach>
</if>
<if test="item.nested != 'true'">
#{item.columnValue}
</if>
</foreach>
order by EMP_NO, START_DATE
</select>
전자정부 표준프레임워크 기반 MyBatis 적용 가이드이다.
표준프레임워크 dataaccess artifact version을 다음과 같이 2.7.0으로 변경한다.
<!-- 실행환경 라이브러리 -->
<dependency>
<groupId>egovframework.rte</groupId>
<artifactId>egovframework.rte.psl.dataaccess</artifactId>
<version>2.7.0</version>
</dependency>
Spring XML 설정 파일 상(ex: context-mapper.xml)에 다음과 같은 sqlSession bean을 추가한다.
Ex) context-mapper.xml
<!-- SqlSession setup for MyBatis Database Layer -->
<bean id="sqlSession" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource" />
<property name="mapperLocations" value="classpath:/sqlmap/mappers/**/*.xml" />
</bean>
MyBatis 가이드에 따라 query xml을 작성한다. (2.1의 예제 상 지정된 mapperLocations 위치)
DAO의 경우는 다음과 같이 3가지 방식이 가능하다.
| 방식 | 설명 | 비고 |
|---|---|---|
| 기존 DAO 클래스 방식 | @Repository 지정 및 EgovAbstractMapper extends 활용 | 기존 iBatis와 같은 방식 |
| Mapper interface 방식 | Mapper 인터페이스 작성 및 @Mapper annotation 지정 | @Mapper는 marker annotation(표준프레임워크 제공) |
| Annotation 방식 | query xml 없이 mapper 인터페이스 상 @Select, @Insert 등을 활용 | Dynamic SQL 등의 사용에 제약이 있음 |
@Repository 지정된 class에 EgovAbstractMapper를 extends 하여 insert, update, delete, selectByPk, list 메서드를 활용한다.
@Repository("deptMapper")
public class DeptMapper extends EgovAbstractMapper {
public void insertDept(String queryId, DeptVO vo) {
insert(queryId, vo);
}
public int updateDept(String queryId, DeptVO vo) {
return update(queryId, vo);
}
public int deleteDept(String queryId, DeptVO vo) {
return delete(queryId, vo);
}
public DeptVO selectDept(String queryId, DeptVO vo) {
return (DeptVO)selectByPk(queryId, vo);
}
@SuppressWarnings("unchecked")
public List<DeptVO> selectDeptList(String queryId, DeptVO searchVO) {
return list(queryId, searchVO);
}
}
Mapper 인터페이스 작성 시 다음과 같이 @Mapper annotation을 사용한다.
(패키지를 포함하는 클래스명 부분이 mapper xml 상의 namespace로 선택되고 인터페이스 메서드가 query id로 호출되는 방식)
@Mapper("employerMapper")
public interface EmployerMapper {
public List<EmpVO> selectEmployerList(EmpVO vo);
public EmpVO selectEmployer(BigDecimal empNo);
public void insertEmployer(EmpVO vo);
public int updateEmployer(EmpVO vo);
public int deleteEmployer(BigDecimal empNo);
}
이 경우는 xml 설정상에 다음과 같은 MapperConfigurer 설정이 필요하다.
Ex: context-mapper.xml
<bean class="egovframework.rte.psl.dataaccess.mapper.MapperConfigurer">
<property name="basePackage" value="egovframework.rte.**.mapper" />
</bean>
basePackage에 지정된 패키지 안에서 @Mapper annotation을 스캔하는 설정이다.
⇒ @Mapper로 지정된 인터페이스를 @Service에서 injection 하여 사용한다.
public class EmployerMapperTest {
@Resource(name = "employerMapper")
EmployerMapper employerMapper;
@Test
public void testInsert() throws Exception {
EmpVO vo = makeVO();
// insert
employerMapper.insertEmployer(vo);
// select
EmpVO resultVO = employerMapper.selectEmployer(vo.getEmpNo());
// check
checkResult(vo, resultVO);
}
}
mapper xml 작성 없이 Mapper 인터페이스 상에 @Select, @Insert, @Update, @Delete 등의 annotation을 통해 query가 지정되어 사용된다.
@Mapper("departmentMapper")
public interface DepartmentMapper {
@Select("select DEPT_NO as deptNo, DEPT_NAME as deptName, LOC as loc from DEPT where DEPT_NO = #{deptNo}")
public DeptVO selectDepartment(BigDecimal deptNo);
@Insert("insert into DEPT(DEPT_NO, DEPT_NAME, LOC) values (#{deptNo}, #{deptName}, #{loc})")
public void insertDepartment(DeptVO vo);
@Update("update DEPT set DEPT_NAME = #{deptName}, LOC = #{loc} WHERE DEPT_NO = #{deptNo}")
public int updateDepartment(DeptVO vo);
@Delete("delete from DEPT WHERE DEPT_NO = #{deptNo}")
public int deleteDepartment(BigDecimal deptNo);
}
⇒ 이 경우는 별도의 mapper xml을 만들 필요는 없지만, dynamic query를 사용하지 못하는 등의 제약사항이 따른다.
Spring Data는 데이터베이스 관련 많은 하위 프로젝트를 포함하는 오픈 소스 프로젝트로, non-relational databases, map-reduce frameworks, and cloud based data services 등의 새로운 데이터 액세스 기술을 보다 쉽게 사용 할 수 있는 기능을 제공한다. 또한 관계형 데이터베이스 기술에 대한 향상된 지원도 제공한다.
| Category | Sub-Project | Description |
|---|---|---|
| Relational Databases | JPA | Spring Data JPA - Simplifies the development of creating a JPA-based data access layer |
| JDBC Extensions | Support for Oracle RAC, Advanced Queuing, and Advanced datatypes. Support for using QueryDSL with JdbcTemplate. | |
| Big Data | Apache Hadoop | The Apache Hadoop project is an open-source implementation of frameworks for reliable, scalable, distributed computing and data \storage. |
| Data-Grid | GemFire | VMware vFabric GemFire is a distributed data management platform providing dynamic scalability, high performance, and database-like \persistence. It blends advanced techniques like replication, partitioning, data-aware routing, and continuous querying. |
| HTTP | REST | Spring Data REST - Perform CRUD operations of your persistence model using HTTP and Spring Data Repositories. |
| Key Value Stores | Redis | Redis is an open source, advanced key-value store. |
| Document Stores | MongoDB | MongoDB is a scalable, high-performance, open source, document-oriented database. |
| Graph Databases | Neo4j | Neo4j is a graph database, a fully transactional database that stores data structured as graphs. |
| Column Stores | HBase | Apache HBase is an open-source, distributed, versioned, column-oriented store modeled after Google’ Bigtable. HBase functionality is part of the Spring for Apache Hadoop project. |
| Common Infrastructure | Commons | Provides shared infrastructure for use across various data access projects. General support for cross-database persistence is located here |
일반적으로 Data Access Layer를 구현하기 위해서는 상당량의 코드를 작성해야 한다. Spring Data에서는 Repository를 추상화하여 다양한 저장소에 접근하기 위한 Data Access Layer 구현 코드를 최소화함으로써 개발생산성을 높일 수 있도록 한다. 이는 Query Method를 통해 가능한데 Query Method란 메소드명만 가지고 쿼리를 만들 수 있다는 것이다. 특정 규칙에 맞게 메소드를 작성하면 개발자가 따로 Data Access 클래스를 만들지 않아도 Spring Data가 대신하여 해당 Database와 자동으로 매핑하여 결과를 가져다준다.
Repository 인터페이스를 상속받아 CRUD 관련 메소드들을 제공하는 CrudRepository 인터페이스와 Paging 처리 기능이 제공되는 PagingAndSortingRepository 인터페이스를 살펴보도록 하겠다.
Spring Data 리파지토리 추상화의 핵심 인터페이스는 바로 Repository이다. Repository는 일종의 마커 인터페이스로 클래스 타입과 ID 타입을 이용해서 작성할 수 있으며 Repository 인터페이스의 하위 인터페이스로 CRUD 기능을 제공하는 CrudRepository가 있다. 다음은 CrudRepository 코드이다.
public interface CrudRepository<T, ID extends Serializable> extends Repository<T, ID> {
<S extends T> S save(S entity);........................❶
T findOne(ID primaryKey);..............................❷
Iterable<T> findAll();.................................❸
Long count();..........................................❹
void delete(T entity);.................................❺
boolean exists(ID primaryKey);.........................❻
}
❶ 전달받은 엔터티를 저장한다.
❷ 전달된 ID로 식별한 엔터티를 리턴한다.
❸ 모든 엔터티를 리턴한다.
❹ 엔터티의 갯수를 리턴한다.
❺ 전달받은 엔터티를 삭제한다.
❻ 전달된 ID에 해당하는 엔터티의 존재여부를 리턴한다.
Spring Data에서는 CrudRepository 이외에도 페이징 처리 기능을 제공하는 PagingAndSortingRespository라는 인터페이스를 제공하며 이 인터페이스는 CrudRepository를 상속받고 있다.
public interface PagingAndSortingRepository<T, ID extends Serializable> extends CrudRepository<T, ID> {
Iterable<T> findAll(Sort sort);
Page<T> findAll(Pageable pageable);
}
PagingAndSortingRepository 인터페이스를 사용하여 2번째 페이지에서 20건의 데이터를 가져오는 예제이다.
PagingAndSortingRepository<User, Long> repository =
Page<User> users = repository.findAll(new PageRequest(1, 20));
JPA 모듈은 문자열로 쿼리를 정의하거나 메서드에서 파생되어진 쿼리를 사용하는 방법을 지원한다.
스트링으로 쿼리를 정의하는 예시 :
public interface UserRepository extends Repository<User, Long> {
List<User> findByEmailAddressAndLastname(String emailAddress, String lastname);
JPA표준 API는 위의 스트링으로 정의된 쿼리를 다음 쿼리로 변경한다.
select u from User u where u.emailAddress = ?1 and u.lastname = ?2
| Keyword | Sample | JPQL snippet |
|---|---|---|
And | findByLastnameAndFirstname | … where x.lastname = ?1 and x.firstname = ?2 |
Or | findByLastnameOrFirstname | … where x.lastname = ?1 or x.firstname = ?2 |
Between | findByStartDateBetween | … where x.startDate between 1? and ?2 |
LessThan | findByAgeLessThan | … where x.age < ?1 |
GreaterThan | findByAgeGreaterThan | … where x.age > ?1 |
After | findByStartDateAfter | … where x.startDate > ?1 |
Before | findByStartDateBefore | … where x.startDate < ?1 |
IsNull | findByAgeIsNull | … where x.age is null |
IsNotNull,NotNull | findByAge(Is)NotNull | … where x.age not null |
Like | findByFirstnameLike | … where x.firstname like ?1 |
NotLike | findByFirstnameNotLike | … where x.firstname not like ?1 |
StartingWith | findByFirstnameStartingWith | … where x.firstname like ?1 (parameter bound with appended %) |
EndingWith | findByFirstnameEndingWith | … where x.firstname like ?1 (parameter bound with prepended %) |
Containing | findByFirstnameContaining | … where x.firstname like ?1 (parameter bound wrapped in %) |
OrderBy | findByAgeOrderByLastnameDesc | … where x.age = ?1 order by x.lastname desc |
Not | findByLastnameNot | … where x.lastname <> ?1 |
In | findByAgeIn(Collection | … where x.age in ?1 |
NotIn | findByAgeNotIn(Collection | … where x.age not in ?1 |
True | findByActiveTrue() | … where x.active = true |
False | findByActiveFalse() | … where x.active = false |
Using named queries to declare queries for entities is a valid approach and works fine for a small number of queries. As the queries themselves are tied to the Java method that executes them you actually can bind them directly using the Spring Data JPA @Query annotation rather than annotating them to the domain class. This will free the domain class from persistence specific information and co-locate the query to the repository interface.
쿼리메서드에 정의된 쿼리들은 xml에 선언된 @NamedQuery나 named queries보다 우선하여 처리됩니다.
@Query를 이용한 쿼리 선언 예제 :
public interface UserRepository extends JpaRepository<User, Long> {
@Query("select u from User u where u.emailAddress = ?1")
User findByEmailAddress(String emailAddress);
}
By default Spring Data JPA will use position based parameter binding as described in all the samples above. This makes query methods a little error prone to refactoring regarding the parameter position. To solve this issue you can use @Param annotation to give a method parameter a concrete name and bind the name in the query: 기본적으로 스프링 데이터 JPA는 위의 모든 샘플에 설명 된대로 파라미터가 바인딩 된 바인딩 위치 기반 매개 변수를 사용합니다.
파라미터를 이용한 쿼리 선언 예제 :
public interface UserRepository extends JpaRepository<User, Long> {
@Query("select u from User u where u.firstname = :firstname or u.lastname = :lastname")
User findByLastnameOrFirstname(@Param("lastname") String lastname, @Param("firstname") String firstname);
}
Spring Data MongoDB는 Spring Data의 하위 프로젝트로서 document-oriented storage를 지원하는 MongoDB에 대한 Data Access 기능을 제공한다.
Spring Data MongoDB를 사용하기 위해서는 다음과 같은 dependency 추가가 필요하다.
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-mongodb</artifactId>
<version>1.7.2.RELEASE</version>
</dependency>
Spring 기반에서는 다음과 같이 Mongo에 대한 인스턴스 생성이 필요하다.
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:mongo="http://www.springframework.org/schema/data/mongo"
xsi:schemaLocation=
"http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-4.0.xsd
http://www.springframework.org/schema/data/mongo
http://www.springframework.org/schema/data/mongo/spring-mongo.xsd
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-4.0.xsd">
<!-- Default bean name is 'mongo' -->
<mongo:mongo-client
host="localhost"
port="27017"
credentials="id:password@database"
id="mongo">
</mongo:mongo-client>
</beans>
추가적으로 MongoDB가 replica 방식으로 굿어된 경우는 다음과 같이 replica-set 설정을 지정하면 된다.
<mongo:mongo id="replicaSetMongo" replica-set="127.0.0.1:27017,localhost:27018"/>
다음으로 Mongo 인스턴스와 연결을 위한 MongoDBFactory이 필요한데, XML 기반의 설정에서는 다음과 같이 지정할 수 있다.
<!-- Default bean name is 'mongoDbFactory' -->
<mongo:db-factory dbname="database" mongo-ref="mongo" id="mongoDbFactory" />
실제 mongoDB에 대한 처리(operations)를 위하여 MongoTemplate을 생성한다.
MontoTemplate의 다은과 같은 생성자를 통해 생성될 수 있다.
XML 설정일 경우는 다음과 같이 생성할 수 있다.
<mongo:mongo-client
host="localhost"
port="27017"
credentials="id:password@database"
id="mongo">
</mongo:mongo-client>
<bean id="mongoTemplate" class="org.springframework.data.mongodb.core.MongoTemplate">
<constructor-arg ref="mongo"/>
<constructor-arg name="databaseName" value="geospatial"/>
</bean>
MongoDbFactory를 사용하는 경우 다음과 같이 지정할 수 있다.
<!-- Default bean name is 'mongo' -->
<mongo:mongo-client
host="localhost"
port="27017"
credentials="id:password@xxx"
id="mongo">
</mongo:mongo-client>
<!-- for Replica Sets -->
<!-- mongo:mongo id="replicaSetMongo" replica-set="127.0.0.1:27017,127.0.0.1:27018" /-->
<!-- Default bean name is 'mongoDbFactory' -->
<mongo:db-factory dbname="database" mongo-ref="mongo" id="mongoDbFactory" />
<bean id="mongoTemplate" class="org.springframework.data.mongodb.core.MongoTemplate">
<constructor-arg name="mongoDbFactory" ref="mongoDbFactory" />
</bean>
MongoTemplate는 저장, 수정, 삭제 및 object 매팽 등의 기본 기능을 제공한다.
다음은 Person 객체에 대한 간단한 기본 예제이다.
public class Person {
private String id;
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getId() {
return id;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
@Override
public String toString() {
return "Person [id=" + id + ", name=" + name + ", age=" + age + "]";
}
}
@Resource(name="mongoTemplate")
private MongoTemplate mongoTemplate;
@Test
public void testBasicOperations() {
Person person = new Person("Joe", 34);
// Insert
mongoTemplate.insert(person);
LOGGER.info("Insert : " + person);
// Find
person = mongoTemplate.findOne(new Query(where("name").is("Joe")), Person.class);
LOGGER.info("Found: " + person);
assertEquals("Joe", person.getName());
// Update
mongoTemplate.updateFirst(query(where("name").is("Joe")), update("age", 35), Person.class);
person = mongoTemplate.findOne(query(where("name").is("Joe")), Person.class);
LOGGER.info("Updated: " + person);
assertEquals(35, person.getAge());
// Delete
mongoTemplate.remove(person);
// Check that deletion worked
List<Person> people = mongoTemplate.findAll(Person.class);
LOGGER.info("Number of people = : " + people.size());
assertEquals(0, people.size());
mongoTemplate.dropCollection("person");
}
※ 내부적으로 MongoConverter에 의해 String과 ObjectId에 대해서는 자동 변환 처리됨
MongoDB는 모든 문서에 대하여 ‘_id’ 필드를 가져야 한다. 그렇지 않은 경우 내부적으로 자동생성되는 ObjectId로 처리된다. 그러나 MongoMappingConverter가 사용되면 다음과 같은 규칙에 의해 ‘_id’ 필드에 대한 매핑을 처리한다.
MongoTemplate에는 객체를 저장하기 위한 몇 개의 메소드를 제공한다. 가장 간단한 경우가 POJO를 저장하는 것이다. 이 경우 collection 이름은 클래스명(not fully qualifed)이 사용되면, 저장 메소스 호출 시 지정될 수도 있다.
다음은 저장 처리 메소드에 대한 정리이다.
※ MongoTemplate은 저장을 위해 save와 insert를 제공하는데, save의 경우는 기존 등록된 문서가 없는 경우 insert로 처리되며 존재하는 경우 덮어쓰기를 한다.
insert의 경우 기존 id가 존재하면 오류를 발생한다.
문서 수정 처리를 위해서는 첫번째 문서만을 수정하는 updateFirst 메소드와 모든 문서를 수정하는 updateMulti로 구성된다.
다음은 계정이 저축계좌(SAVINGS)인 모든 계좌에 50원을 추가하는 예제이다.
import static org.springframework.data.mongodb.core.query.Criteria.where;
import static org.springframework.data.mongodb.core.query.Query;
import static org.springframework.data.mongodb.core.query.Update;
...
WriteResult wr = mongoTemplate.updateMulti(
new Query(where("accounts.accountType").is(Account.Type.SAVINGS)),
new Update().inc("accounts.$.balance", 50.00),
Account.class);
MongoDB에 대한 기본 처리는 MongoDB Manual을 참조하고, Spring과의 연동 부분에 대한 세부적인 처리는 Spring Data MongoDB에 대한 Reference를 참조한다.
Spring Data MongoDB도 Spring Data repository 추상화 인터페이스를 지원한다. 이에 대한 내용은 Spring Data JPA 가이드 중 Repository 부분을 참조한다.
MongoDB에 대한 repository를 사용하기 위해서는 다음과 같은 mongo schem의 repositories 설정이 필요하다.
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:mongo="http://www.springframework.org/schema/data/mongo"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
http://www.springframework.org/schema/data/mongo http://www.springframework.org/schema/data/mongo/spring-mongo.xsd">
<mongo:mongo-client
host="localhost"
port="27017"
credentials="id:password@database" >
</mongo:mongo-client>
<mongo:db-factory dbname="database" mongo-ref="mongo" />
<bean id="mongoTemplate" class="org.springframework.data.mongodb.core.MongoTemplate">
<constructor-arg name="mongoDbFactory" ref="mongoDbFactory" />
</bean>
<!-- for Repository -->
<mongo:repositories base-package="egovframework.rte.psl.data.mongodb.repository" />
</beans>
일반적인 POJO를 통해 MongoDB에 문서를 저장한다.
public class Person {
@Id
private String id;
private String firstname;
private String lastname;
private Address address;
private double[] location;
...
// getters/setters
public interface PersonRepository extends MongoRepository<Person, Long> {
// additional custom finder methods go here
}
MongoRepository interface는 기본적으로 PagingAndSortingRepository를 extends하기 때문에 다음과 같은 기본 페이징 처리를 지원한다.
@Autowired
private PersonRepository repository;
...
@Test
public void readsFirstPageCorrectly() {
Page<Person> persons = repository.findAll(new PageRequest(0, 10));
LOGGER.info("Persons Total elements : " + persons.getTotalElements());
assertTrue(persons.isFirst());
}
Repository는 Spring Data에서 제공되는 keyword 기반의 query method를 사용할 수 있다.
ex:
public interface PersonRepository extends MongoRepository<Person, String> {
List<Person> findByLastname(String lastname);
Page<Person> findByFirstname(String firstname, Pageable pageable);
Person findByShippingAddresses(Address address);
}
지원되는 keyword는 다음과 같다.
| Keyword | Sample | Logic result |
|---|---|---|
| GreaterThan | findByAgeGreaterThan(int age) | {“age” : {“$gt” : age}} |
| GreaterThanEqual | findByAgeGreaterThanEqual(int age) | {“age” : {“$gte” : age}} |
| LessThan | findByAgeLessThan(int age) | {“age” : {“$lt” : age}} |
| LessThanEqual | findByAgeLessThanEqual(int age) | {“age” : {“$lte” : age}} |
| Between | findByAgeBetween(int from, int to) | {“age” : {“$gt” : from, “$lt” : to}} |
| In | findByAgeIn(Collection ages) | {“age” : {“$in” : [ages…]}} |
| NotIn | findByAgeNotIn(Collection ages) | “age” : {“$nin” : [ages…]}} |
| IsNotNull, NotNull | findByFirstnameNotNull() | {“age” : {“$ne” : null}} |
| IsNull, Null | findByFirstnameNull() | {“age” : null} |
| Like | findByFirstnameLike(String name) | {“age” : age} ( age as regex) |
| Regex | findByFirstnameRegex(String firstname) | {“firstname” : {“$regex” : firstname }} |
| (No keyword) | findByFirstname(String name) | {“age” : name} |
| Not | findByFirstnameNot(String name) | {“age” : {“$ne” : name}} |
| Near | findByLocationNear(Point point) | {“location” : {“$near” : [x,y]}} |
| Within | findByLocationWithin(Circle circle) | {“location” : {“$within” : {“$center” : [ [x, y], distance]}}} |
| Within | findByLocationWithin(Box box) | {“location” : {“$within” : {“$box” : [ [x1, y1], x2, y2]}}} |
| IsTrue, True | findByActiveIsTrue() | {“active” : true} |
| IsFalse, False | findByActiveIsFalse() | {“active” : false} |
| Exists | findByLocationExists(boolean exists) | {“location” : {“$exists” : exists }} |
위의 keyword를 delete..By 또는 remove..By 형태로 사용하는 경우 삭제 처리가 가능하다.
public interface PersonRepository extends MongoRepository<Person, String> {
List <Person> deleteByLastname(String lastname);
Long deletePersonByLastname(String lastname);
}
Near keyword를 사용하는 경우 geo-spatial 처리와 관련된 query도 처리 가능하다.
public interface PersonRepository extends MongoRepository<Person, String> {
// { 'location' : { '$near' : [point.x, point.y], '$maxDistance' : distance}}
List<Person> findByLocationNear(Point location, Distance distance);
}
org.springframework.data.mongodb.repository.Query anntation을 사용할 경우 JSON query 방식을 통해 직접 query를 지정할 수 있다.
public interface PersonRepository extends MongoRepository<Person, String> {
@Query("{ 'firstname' : ?0 }")
List<Person> findByThePersonsFirstname(String firstname);
}
객체 모델링(Object Oriented Modeling)과 관계형 데이터 모델링(Relational Data Modeling) 사이의 불일치를 해결해 주는 OR Mapping 서비스로 자바 표준인 JPA를 표준 서비스로 제시하고 구현체로는 JPA 구현체중에 가장 성능이 우수한 것으로 알려진 Hibernate를 이용하였다. 서비스의 특징을 살펴보면 다음과 같다.

옆의 그림에서 보는것과 같이 DBMS 기반의 어플리케이션 수행을 하기 위해 필요한 주요 구성 요소는 Entity, Persistence.xml 이며, 각각은 다음과 같은 역할을 수행한다.
ORM 서비스에 대한 자세한 설명에 앞서 간단하게 ORM 서비스를 시작하는데 필요한 것에 대한 설명을 하고자 한다.
본 서비스를 활용하기 위해서 필요한 Library 목록과 설명은 아래와 같다.
| 라이브러리 | 설명 | 연관 라이브러리 |
|---|---|---|
| antlr-2.7.7.jar | 파서 라이브러리 | |
| commons-collections-3.2.jar | collection 처리를 위한 라이브러리 | |
| commons-dbcp-1.2.2.jar | DataSource 커넥션 풀 라이브러리 | |
| commons-logging-1.1.1.jar | Logging 처리를 위한 라이브러리 | hibernate-annotations-3.4.0.GA.jar에서 참조 |
| log4j-1.3alpha-8.jar | Logging 처리를 위한 라이브러리 | |
| slf4j-api-1.5.3.jar | Logging 처리를 위한 라이브러리 | |
| slf4j-log4j12-1.5.3.jar | Logging 처리를 위한 라이브러리 | |
| commons-pool-1.3.jar | pooling 처리를 위한 라이브러리 | commons-dbcp-1.2.2.jar에서 참조 |
| dom4j-1.6.1.jar | XML 파싱 라이브러리 | hibernate-3.2.4.ga.jar 에서 참조 |
| ejb3-persistence-1.0.2.GA.jar | JPA Interface 클래스 라이브러리 | |
| hibernate-annotations-3.4.0.GA.jar | Hibernate Annotation | |
| hibernate-entitymanager-3.4.0.GA.jar | Hibernate Entity Manager 구현체 라이브러리 | |
| hibernate-commons-annotations-3.1.0.GA.jar | Hibernate 공통 annotation 라이브러리 | hibernate-entitymanager-3.4.0.GA.jar에서 참조 |
| hibernate-core-3.3.0.SP1.jar | Hiberante Core 라이브러리 | hibernate-entitymanager-3.4.0.GA.jar에서 참조 |
| javassist-3.4.GA.jar | 자바 bytecode 조작 라이브러리 | hibernate-entitymanager-3.4.0.GA.jar에서 참조 |
| jta-1.1.jar | JTA 인터페이스 라이브러리 | hibernate-entitymanager-3.4.0.GA.jar에서 참조 |
| hsqldb-1.8.0.10.jar | HSQL JDBC 드라이버 | |
| mysql-connector-java-5.1.6.jar | MYSQL JDBC 드라이버 | |
| ojdbc-14.jar | ORACLE JDBC 드라이버 | |
| junit-4.4.jar | 테스트 지원 라이브러리 |
간단한 형태의 Entity 클래스를 생성한다. 네개의 Attribute로 구성되어 있고 각각의 getter,setter 메소드로 구성되어 있다.
@Entity
public class Department implements Serializable {
private static final long serialVersionUID = 1L;
@Id
private String deptId;
private String deptName;
private Date createDate;
private BigDecimal empCount;
public String getDeptId() {
return deptId;
}
public void setDeptId(String deptId) {
this.deptId = deptId;
}
...
}
위에서 정의한 Entity 클래스를 가지고 JPA 수행하기 위한 프로퍼티 파일로 구현체제공 클래스정보,엔티티클래스정보,DB접속 정보,로깅정보,테이블자동생성정보를 포함하고 있다.
<persistence-unit name="PersistUnit" transaction-type="RESOURCE_LOCAL">
<provider>org.hibernate.ejb.HibernatePersistence</provider>
<class>egovframework.Department</class>
<exclude-unlisted-classes/>
<properties>
<property name="hibernate.connection.driver_class" value="org.hsqldb.jdbcDriver"/>
<property name="hibernate.connection.url" value="jdbc:hsqldb:mem:testdb"/>
<property name="hibernate.connection.username" value="sa"/>
<property name="hibernate.dialect" value="org.hibernate.dialect.HSQLDialect"/>
<property name="hibernate.connection.autocommit" value="false"/>
<property name="hibernate.show_sql" value="true"/>
<property name="hibernate.format_sql" value="true"/>
<property name="hibernate.hbm2ddl.auto" value="create"/>
</properties>
</persistence-unit>
위에서 정의한 Department를 이용하여 입력,수정,조회,삭제 처리를 하는 것을 JUNIT형태로 구성하였다.
@Test
public void testDepartment() throws Exception {
String modifyName = "Marketing Department";
String deptId = "DEPT-0001";
Department department = makeDepartment(deptId);
// Entity Manager 생성
emf = Persistence.createEntityManagerFactory("PersistUnit");
em = emf.createEntityManager();
// 입력
em.getTransaction().begin();
em.persist(department);
em.getTransaction().commit();
em.getTransaction().begin();
Department departmentAfterInsert = em.find(Department.class, deptId );
// 입력 확인
assertEquals("Department Name Compare!",department.getDeptName(),departmentAfterInsert.getDeptName());
// 수정
departmentAfterInsert.setDeptName(modifyName);
em.merge(departmentAfterInsert);
em.getTransaction().commit();
em.getTransaction().begin();
Department departmentAfterUpdate = em.find(Department.class, deptId );
// 수정 확인
assertEquals("Department Modify Name Compare!",modifyName,departmentAfterUpdate.getDeptName());
// 삭제
em.remove(departmentAfterUpdate);
em.getTransaction().commit();
// 삭제 확인
Department departmentAfterDelete = em.find(Department.class, deptId );
assertNull("Department is Deleted!",departmentAfterDelete);
em.close();
}
ORM 서비스를 구성하는 가장 기초적인 클래스로 어플리케이션에서 다루고자 하는 테이블에 대응하여 구성할 수 있으며 테이블이 포함하는 컬럼에 대응한 속성들을 가지고 있다.
Entity를 구성하기 위한 아래와 같은 요건이 있다.(JPA요건)
@Entity
public class User {
}
public User(){
}
@Id
private String userId;
public class User implements Serializable {
private static final long serialVersionUID = -8077677670915867738L;
}
private String userName;
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
Entity를 구성하는 주요한 Annotation은 다음과 같다.
해당 클래스가 Entity 클래스임을 표시하는 것으로 클래스 선언문 위에 기재한다. 테이블명과 Entity명이 다를 때에는 name에 해당 테이블명을 기재한다
@Entity(name="USER_TB")
public class User {
}
해당 Attribute가 Key임을 표시하는 것으로 Attribute 위에 기재한다.
@Id
private String userId;
해당 Attribute와 매핑되는 컬럼정보를 입력하기 위한 것으로 Attribue위에 기재한다. 컬럼명과 Attribute명이 일치할 경우는 기재하지 않아도 됨
@Column(name = "DEPT_NAME", length = 30)
private String deptName;
테이블간 관계를 구성하기 위한 것으로 정의되는 Attribute위에 기재한다. 각각은 1:1,1:N,N:1,N;N의 관계를 표현함. 이에 대한 자세한 설명은 association_mapping 참고
@ManyToMany
private Set<Role> roles = new HashSet(0);
테이블의 컬럼과 매핑되지 않고 쓰이는 Attribute를 정의하고자 할때 Attribute위에 기재한다.
@Transient
private String roleName;
ORM서비스를 이용하여 특정 DB에 데이터를 입력, 수정, 조회, 삭제, 배치입력하는 방법에 대해 알아보도록 한다.
EntityManager의 persist()메소드를 호출하여 DB에 단건의 데이터를 추가할 수 있다. persist() 메소드 호출시 대상이 되는 Entity를 입력 인자로 전달해야 한다.
private Department addDepartment() throws Exception {
// 1. insert a new Department information
Department department = new Department();
String DepartmentId = "DEPT-0001";
department.setDeptId(DepartmentId);
department.setDeptName("SaleDept");
department.setDesc("판매부서");
em.persist(department);
...
return department;
}
위의 예를 보면 EntityManager의 persist() 메소드에 department라는 Entity를 입력인자로 전달하여 처리하였다.
수정을 하고자 할때는 두가지 방법으로 가능한데 우선 EntityManager의 merge()메소드를 호출하여 DB에 단건의 데이터를 수정할 수 있고, 특정 객체가 Persistent 상태이고, 동일한 트랜잭션 내에서 해당 객체의 속성 값에 변경이 발생한 경우 merge() 메소드를 직접적으로 호출하지 않아도 트랜잭션 종료 시점에 변경 여부가 체크되어 변경 사항이 DB에 반영된다.
public void testUpdateDepartment() throws Exception {
// 1. insert a new Department information
Department department = addDepartment();
// 2. update a Department information
department.setDeptName("Purchase Dept");
// 3. 명시적인 메소드 호출
em.merge(department);
}
위의 예를 보면 EntityManager의 merge() 메소드에 department라는 Entity를 입력인자로 전달하여 처리하였다.
public void testUpdateDepartment() throws Exception {
// 1. insert a new Department information
Department department = addDepartment();
// 2. update a Department information
department.setDeptName("Purchase Dept");
// commit. successful update!!!
}
위의 예를 보면 setDeptName()을 통해서 변경하고 Commit 처리시 변경된다.
EntityManager의 find()메소드를 호출하여 DB에서 원하는 한건의 데이터를 조회할 수 있다. find() 메소드 호출시 대상이 되는 Entity의 Id를 입력 인자로 전달해야 한다.
private void assertDepartmentInfo(String departmentId, Department department)
throws Exception {
Department result = (Department) em.find(Department.class, departmentId);
//...
}
위의 예를 보면 EntityManager의 find() 메소드에 departmentId라는 Entity Id를 입력인자로 전달하여 처리하였다.
EntityManager의 remove()메소드를 호출하여 DB에서 원하는 한건의 데이터를 조회할 수 있다. remove() 메소드 호출시 대상이 되는 Entity를 입력 인자로 전달해야 한다.
public void testDeleteDepartment() throws Exception {
// 1. insert a new Department information
Department department = addDepartment();
// 2. delete a Department information
em.remove(department);
}
위의 예를 보면 EntityManager의 remove() 메소드에 department라는 Entity를 입력인자로 전달하여 처리하였다. 그러나 주의할 점은 위의 예에서는 department 객체는 DB에 등록처리한 객체와 동일 객체이기에 그대로 remove를 쓸 수 있지만 키만 동일하게 신규로 객체를 만든경우에는 remove를 바로 쓸수 없다. 그럴 경우에는 아래와 같은 방법으로 처리해야 한다.
public void testDeleteDepartment() throws Exception {
Department department = new Department();
department.setDeptId = "DEPT_1";
// 2. delete a Department information
em.remove(em.getReference(Department.class, department.getDeptId()));
}
위의 예에서는 getReference 메소드를 호출하여 Entity의 Id에 해당하는 객체 정보를 추출하여 그정보를 입력인자로 해서 remove를 호출하였다.
EntityManager의 persist()메소드를 호출하여 DB에 입력하고 loop를 통해 반복적으로 수행한다. OutOfMemoryException 방지를 위해서 일정한 term을 두고 flush(),clear()을 호출하여 메모리에 있는 사항을 삭제한다.
public void testMultiSave() throws Exception {
for (int i = 0; i < 900 ; i++) {
Department department = new Department();
String DeptId = "DEPT-000" + i;
department.setDeptId(DeptId);
department.setDeptName("Sale" + i);
department.setDesc("판매부" + i);
em.persist(department);
logger.debug("=== DEPT-000"+i+" ===");
// OutOfMemoryException 피하기 위해서
if (i != 0 && i % 9 == 0) {
em.flush();
em.clear();
}
}
}
엔티티 클래스에 정의하거나 EntityListener를 지정하여 콜백함수를 정의하여 실제 엔티티 Operation 직전 직후에 비지니스 로직 체크등의 로직을 별도 분리하여 처리하도록 지원한다.
| 메소드명 | 설 명 | 관련 Operation |
|---|---|---|
| PrePersist | Persist이전 시점에 수행 | persist |
| PostPersist | Persist이후 시점에 수행 | persist |
| PreRemove | Remove이전 시점에 수행 | remove |
| PostRemove | Remove이후 시점에 수행 | remove |
| PreUpdate | Update이전 시점에 수행 | merge |
| PostUpdate | Update이후 시점에 수행 | merge |
| PostLoad | Find 이후 시점에 수행 | find |
콜백 함수를 Entity 클래스에 직접 Annotation을 기재하여 Method를 정의할 수 있다.
@Entity
public class User {
@PrePersist
@PreUpdate
protected void validateCreate() throws Exception {
if (getSalary() < 2000000 )
throw new Exception("Insufficient Salary !");
}
}
위의 예를 보면 Persist, Update 이전 시점에 Salary가 2,000,000 이하가 되는지 여부를 체크를 하도록 한다. Update의 경우는 EntityManager의 merge()를 이용하여 하는 것과 ql을 이용하여 Update 수행하는 것 모두에 해당한다.
@Test
public void testUpdateFailUser() throws Exception {
newTransaction();
User getUser = (User) em.find(User.class, "User1");
assertEquals(getUser.getSalary(), sal );
user.setSalary(1000000);
em.merge(user);
// 2. Update User , 위의 merge() 호출이 아닌 Transaction 종료시 Update수행됨.
try{
closeTransaction();
}
catch( Exception e ){
e.printStackTrace();
assertTrue("fail to PreUpdate.",e instanceof Exception);
}
}
위의 예를 보면 salary가 2000000 이하로 설정되어 Update될 경우 Exception 이 발생하는 것을 알 수 있다.
@Test
public void testUpdateFail2User() throws Exception {
newTransaction();
User getUser = (User) em.find(User.class, "User1");
StringBuffer ql = new StringBuffer();
ql.append("UPDATE User user ");
ql.append("SET user.salary = :salary ");
ql.append("WHERE user.userId = :userId ");
Query query = em.createQuery(ql.toString());
query.setParameter("salary", 1000000);
query.setParameter("userId",getUser.getUserId());
// 2. Update User , 위의 merge() 호출이 아닌 Transaction 종료시 Update수행됨.
try{
closeTransaction();
}
catch( Exception e ){
e.printStackTrace();
assertTrue("fail to PreUpdate.",e instanceof Exception);
}
}
위의 예를 보면 salary가 2000000 이하로 설정되어 Update될 경우 Exception 이 발생하는 것을 알 수 있다. ql를 이용한 Update를 처리할 때도 동일하게 Exception이 발생함을 확인할 수 있다.
엔티티 클래스에 EntityListener를 지정하고 EntityListener에서 Annotation을 기재하여 Method를 정의할 수 있다.
@Entity
@EntityListeners(egovframework.sample.model.callback.AlertMonitor.class)
public class User {
}
//위에서 정의한 AlertMonitor 클래스
public class AlertMonitor {
@PostPersist
public void newUserAlert(User user) {
System.out.println(user.getUserName()+" Created!");
}
@PostLoad
public void usrGetAlert(User user) {
System.out.println(user.getUserName()+" Get!");
}
}
정의하는 위치가 다르고 원래 엔티티를 매개변수로 넘겨야 하는 부분이 차이가 있지만 엔티티 클래스에 지정하는 것과 동일하게 작동한다.
두 클래스 사이의 Association 유형별 매핑 방법에 대해서 살펴보고자 한다. 그리고 다양한 Collection의 매핑 방법 및 Collection의 주요속성인 inverse,cascade에 대해서 샘플코드를 중심으로 살펴본다.
테이블간 1:1 매핑이 있을 경우에 각각의 Entity 클래스를 정의하고 클래스간 관계를 OneToOne 매핑으로 처리한다.
@Entity
public class Employee {
@OneToOne
private TravelProfile profile;
}
@Entity
public class TravelProfile {
@OneToOne
private Employee employee;
}
위의 예를 보면 Employee 와 TravelProfile가 각각 OneToOne이라는 Annotation을 기재하여 매핑처리한 것을 알수 있다.
테이블간 1:N 매핑이 있을 경우에 각각의 Entity 클래스를 정의하고 한쪽에는 OneToMany, 다른쪽에는 ManyToOne 이라는 Annotation을 기재하여 관계를 나타낸다.
@Entity
public class Department{
@OneToMany(targetEntity=User.class)
private Set<User> users = new HashSet(0);
}
@Entity
public class User{
@ManyToOne
private Department department;
}
위의 예를 보면 Department:User = 1:N 의 관계가 있으며 그 관계에 대해서 Department 클래스에서 OneToMany로 표시하고 User 클래스에서 ManyToOne으로 표시하여 관계를 나타냈다.
Collection은 위의 예에서 사용된 Set 이외에도 List,Map를 사용할 수 있는데 각각의 사용법은 다음과 같다
java.util.Set 타입으로 <set>을 이용하여 정의한다. 객체의 저장 순서를 알 수 없으며, 동일 객체의 중복 저장을 허용하지 않는다. (HashSet 이용) 다음은 set 태그를 이용하여 Collection 객체를 정의한 소스 코드의 예이다
@Entity
public class Department{
@OneToMany(targetEntity=User.class)
private Set<User> users = new HashSet(0);
}
java.util.List 타입으로 <list>를 이용하여 정의한다. List 타입의 경우 저장된 객체의 순서를 알 수 있으며, 저장 순서를 테이블에 보관하기 위해서 별도 인덱스 컬럼 정의가 필요하다. (ArrayList 이용) 다음은 list 태그를 이용하여 Collection 객체를 정의한 소스 코드의 예이다
@Entity
public class Department{
@OneToMany(targetEntity=User.class )
private List<User> users = new ArrayList(0);
}
java.util.map 타입으로 <map>을 이용하여 (키,값)을 쌍으로 정의한다. (HashMap 이용) 다음은 map 태그를 이용하여 Collection 객체를 정의한 소스 코드의 예이다. Key 설정을 위해 MapKey라는 Annotation을 추가적으로 정의해야 한다.
@Entity
public class Department{
@OneToMany(targetEntity=User.class)
@MapKey(name="userId")
private Map<String,User> users ;
}
1:N(부모:자식)관계 지정에 있어서 자식쪽에서 부모에 대한 참조 관계를 가지고 있느냐 없느냐에 따라서 참조관계가 있으면 양방향 관계, 없으면 단방향 관계로 정의된다.
자식 Entity에 부모 Entity에 대한 참조정보 없이 정의한다.
@Entity
public class Department{
@OneToMany(targetEntity=User.class)
private Set<User> users = new HashSet(0);
}
@Entity
public class User{
@Column(name="DEPT_ID")
private String deptId;
}
위의 예에서 보면 User 클래스에서 Department 클래스에 대한 참조관계를 지정하지 않고 단순하게 테이블의 컬럼 DEPT_ID와의 매핑으로 deptId를 지정한 것을 알 수 있다.
자식 Entity에 부모 Entity에 대한 참조정보를 지정하여 정의한다.
@Entity
public class Department{
@OneToMany(targetEntity=User.class)
private Set<User> users = new HashSet(0);
}
@Entity
public class User{
@ManyToOne
private Department department;
}
위의 예에서 보면 User 클래스에서 Department 클래스에 대한 ManyToOne Annotation을 통해 매핑관계를 지정한 것을 알 수 있다.
mappedBy와 cascade는 Collection 정의시 중요한 의미를 가지는 속성 중의 하나로, 다음과 같은 의미를 지닌다.
mappedBy와 cascade를 모두 정의하여 사용할 경우에는 자식 Entity에서 관계연결 처리를하고 부모 Entity에서 CUD처리시 자식 Entity도 자동으로 처리된다.
@Entity
public class Department{
@OneToMany(targetEntity=User.class,mappedBy="deptId",cascade={CascadeType.PERSIST, CascadeType.MERGE})
private Set<User> users = new HashSet(0);
}
@Entity
public class User{
@ManyToOne(targetEntity=Department.class)
private Department department;
}
// User(자식) Entity의 setDepartment 메소드를 통해서 관계설정
user.setDepartment(department);
// 부모 entity의 저장으로 자식까지 동시처리
em.persist(department);
mappedBy만 정의하여 사용할 경우에는 자식 Entity에서 관계연결 처리를하고 부모,자식 각자 CUD처리한다.
@Entity
public class Department{
@OneToMany(targetEntity=User.class,mappedBy="deptId")
private Set<User> users = new HashSet(0);
}
@Entity
public class User{
@ManyToOne(targetEntity=Department.class)
private Department department;
}
// User(자식) Entity의 setDepartment 메소드를 통해서 관계설정
user.setDepartment(department);
// 부모/자식 entity의 각각 처리
em.persist(department);
em.persist(user);
cascade만 정의하여 사용할 경우에는 부모 Entity에서 관계연결 처리를 하고 부모 Entity에서 CUD처리시 자식 Entity도 자동으로 처리된다.
@Entity
public class Department{
@OneToMany(targetEntity=User.class,cascade={CascadeType.PERSIST, CascadeType.MERGE})
private Set<User> users = new HashSet(0);
}
@Entity
public class User{
@ManyToOne(targetEntity=Department.class)
private Department department;
}
// Department(부모) Entity의 getUsers().add 메소드를 통해서 관계설정
department.getUsers().add(user);
// 부모 entity의 저장으로 자식까지 동시처리
em.persist(department);
모두 정의하지 않을때는 부모 Entity에서 관계 연결 처리를 하고 부모,자식 각자 CUD처리한다.
@Entity
public class Department{
@OneToMany(targetEntity=User.class)
private Set<User> users = new HashSet(0);
}
@Entity
public class User{
@ManyToOne(targetEntity=Department.class)
private Department department;
}
// Department(부모) Entity의 getUsers().add 메소드를 통해서 관계설정
department.getUsers().add(user);
// 부모/자식 entity의 각각 처리
em.persist(department);
em.persist(user);
테이블간 M:N 매핑이 있을 경우에 각각의 Entity 클래스를 정의하고 양쪽에 ManyToMany이라는 Annotation을 기재하여 관계를 나타낸다.
@Entity
public class Role{
@ManyToMany(targetEntity=User.class)
private Set<User> users = new HashSet(0);
}
@Entity
public class User{
@ManyToMany
@JoinTable(name="AUTHORITY",
joinColumns=@JoinColumn(name="USER_ID"),
inverseJoinColumns=@JoinColumn(name="ROLE_ID"))
private Set<Role> roles = new HashSet(0);
}
위의 예를 보면 Role:User = M:N 의 관계가 있으며 그 관계에 대해서 Role클래스에서 ManyToMany로 표시하고 User 클래스에서 ManyToMany로 표시하여 관계를 나타내면서 User 클래스에서 관계를 위한 별도의 테이블에 대한 정의를 하고 있다. 이 경우에 ROLE과 USER를 연결하는 관계 테이블로 AUTHORITY가 사용된 것을 알 수 있다.
JPA에는 별도의 Query Language를 제공함으로써 객체 지향 관점에서 특정 객체에 대한 조회와 DB 유형에 독립적인 Query 정의를 가능하도록 한다. 구성요소 및 작성 방법은 아래와 같다.
QL Statement 유형으로는 SELECT 문과 Update and Delete 문 두가지가 있다.
조회 결과값을 구체적으로 명시하고자 할 경우 정의한다.
SELECT [object 또는 property], Aggregate Funtions , ...
여러 건의 데이터를 조회할 경우 조회 결과값을 List, Map 또는 사용자 정의 Type으로 정의 가능하다. (Default = Object[])
SELECT new List(prop1, prop2, …)
가능한 Aggregate Funtions
COUNT : Long 으로 리턴
MAX, MIN: 정의된 필드로 리턴
AVG : Double로 리턴
SUM : integral type의 경우는 Long, float type의 경우는 Double, BigInteger는 BigInteger, BigDecimal 은 BigDecimal
QL에서 사용 가능한 주요 Function
| 함수명 | 설명 |
|---|---|
| CONCAT(str1, str2) | 두개의 문자열을 연결한다. |
| SUBSTRING(str, idx, length) | 문자열의 지정한 idx 위치에서 length만큼의 문자열을 얻어낸다. |
| TRIM([type] str) | 문자열의 앞뒤 공백을 삭제한다. (Type이 BOTH일 경우 앞뒤공백 삭제, Type이 LEADING일 경우 앞 공백 삭제, Type이 TRAILING일 경우 뒤 공백 삭제) |
| LOWER(str) | 소문자로 변환한다. |
| UPPER(str) | 대문자로 변환한다. |
| LENGTH(str) | 문자열의 전체 길이를 구한다. |
| LOCATE(str, s, idx) | 해당 문자열 str에서 정의된 문자열 s가 포함되어 있는 위치를 구한다. 검색 시작 위치는 idx이다. |
| 함수명 | 설 명 |
|---|---|
| ABS(num) | 숫자의 절대값을 구한다. |
| SQRT(num) | 숫자의 제곱근을 구한다. |
| MOD(num1,num2) | num2을 num2로 나눈 나머지값을 구한다. |
| SIZE(collection value) | Collection의 포함 엔트리 숫자를 구한다. |
| 함수명 | 설 명 |
|---|---|
| CURRENT_DATE | 현재 날짜를 구한다. |
| CURRENT_TIME | 현재 시간을 구한다. |
| CURRENT_TIMESTAMP | 현재 날짜 및 시간을 구한다. |
조회 대상 객체를 정의하며, SELECT 절이 생략되었을 경우 FROM 절에 정의된 객체가 전달 대상이 된다.
FROM [object] ((AS) alias), …
FROM 절에 JOIN 을 쓸 수 있는데 JOIN의 종류는 다음과 같다.
| JOIN 종류 | 예 제 | 설 명 |
|---|---|---|
| Inner Joins | SELECT c FROM Customer c JOIN c.orders o WHERE c.status = 1 | 비교 대상 양쪽 모두 존재하는 것 추출(Order를 낸 고객만 추출) |
| Left Outer Joins | SELECT c FROM Customer c LEFT JOIN c.orders o WHERE c.status = 1 | 한쪽에만 존재하더라도 추출(Order를 내지않은 고객도 추출) |
| Fetch Joins | SELECT d FROM Department d LEFT JOIN FETCH d.employees WHERE d.deptno = 1 | FETCH문 뒤에 오는 자식리스트도 같이 추출 (Department의 attribute로 가지고 있는 employee 목록 추출, lazy로딩의 경우 Fetch를 하지 않으면 employee정보는 추출되지 않음) |
조회 결과 영역을 보다 상세히 구분하고자 할 경우 정의한다.
WHERE [condition], …
조건을 나타내는 여러 표현이 있는데 그중에 대표적인 것은 다음과 같다. Outer pipes Cell padding No sorting
| 조건 | 설명 | 예 |
|---|---|---|
| Path Expressions | entity 클래스의 attribute를 지칭함. | user.roles |
| Named Parameters | 이름을 지정한 파라미터를 지정할 수 있고 setParameter를 통해서 값을 지정함. | WHERE department.deptName like :condition |
| Positional Parameters | 위치를 지정한 파라미터를 지정할 수 있고 setParameter를 통해서 값을 지정함. | WHERE role.roleName = ?1 |
| Collection Member Expressions | Collection 타입의 Attribute를 ”[NOT] MEMBER [OF]” 라는 표현으로 조건처리함. | user MEMBER OF role.users |
그외에도 IN, LIKE, IS NULL, EXISTS, Function 등의 표현이 지원된다.
조회 결과의 정렬 방법을 정의한다.
ORDER BY [condition] (ASC 또는 DESC), …
조회 결과를 특정 기준으로 그룹핑하고자 할 경우 정의한다
GROUP BY [condition], …
[HAVING] [condition]
대표적인 사용 방법을 예제소스 기반으로 설명하고자 한다. 기본적인 CRUD 방법과 Join 방법은 다음과 같다
QL을 통해 하나의 테이블을 대상으로 조회 작업을 수행할 수 있다.
StringBuffer qlBuf = new StringBuffer();
qlBuf.append("FROM Department department ");
qlBuf.append("WHERE department.deptName like :condition ");
qlBuf.append("ORDER BY department.deptName");
Query qlQuery = em.createQuery(qlBuf.toString());
qlQuery.setParameter("condition", "%%");
List departmentList = qlQuery.getResultList();
위와 같이 정의된 QL문을 통해 조회 조건에 맞는 Department 객체의 List가 리턴된다. WHERE절의 조회 조건은 객체명.Attribute명(department.deptName)으로 정의할 수 있으며 ‘:‘을 사용하여 Named Paramenter를 통해 조회 조건을 완성할 수 있다. 조회 조건의 값은 Query의 setParameter() 메소드를 통해 지정해 주고 있다.
INNER JOIN 과 LEFT OUTER JOIN 을 수행할 수 있고 그 예는 다음과 같다.
StringBuffer qlBuf = new StringBuffer();
qlBuf.append("SELECT user ");
qlBuf.append("FROM User user join user.roles role ");
qlBuf.append("WHERE role.roleName = ?1");
Query query = em.createQuery(qlBuf.toString());
query.setParameter(1, "Admin");
List userList = query.getResultList();
위와 같이 정의된 QL문을 통해 조회 조건에 맞는 Department 객체의 List가 리턴된다. FROM절에서 JOIN을 이용하여 INNER JOIN 처리를 했고 WHERE절의 조회 조건은 객체명.Attribute명(department.deptName)으로 정의할 수 있으며 ‘?!‘를 사용하여 Positional Paramenter를 통해 조회 조건을 완성할 수 있다. 조회 조건의 값은 Query의 setParameter() 메소드를 통해 지정해 주고 있다.
StringBuffer qlBuf = new StringBuffer();
qlBuf.append("SELECT distinct user ");
qlBuf.append("FROM User user, Department department ");
qlBuf.append("WHERE user.department.deptId = department.deptId ");
qlBuf.append("AND department.deptId = :condition1 ");
qlBuf.append("AND user.userName like :condition2 ");
Query query = em.createQuery(qlBuf.toString());
query.setParameter("condition1", "Dept1");
query.setParameter("condition2", "%%");
List userList = query.getResultList();
위와 같이 정의된 QL문을 통해 조회 조건에 맞는 Department 객체의 List가 리턴된다. WHERE절에서 ‘=‘를 통해 INNER JOIN 처리를 했고 조회 조건은 객체명.Attribute명(department.deptName)으로 정의할 수 있으며 ‘?!‘를 사용하여 Positional Paramenter를 통해 조회 조건을 완성할 수 있다. 조회 조건의 값은 Query의 setParameter() 메소드를 통해 지정해 주고 있다.
StringBuffer qlBuf = new StringBuffer();
qlBuf.append("SELECT distinct role ");
qlBuf.append("FROM Role role left outer join role.users user ");
qlBuf.append("ORDER BY role.roleName ASC ");
Query query = em.createQuery(qlBuf.toString());
List roleList = query.getResultList();
위와 같이 정의된 QL문을 통해 조회 조건에 맞는 role객체의 List가 리턴된다. FROM절에서 LEFT OUTER JOIN 처리를 했다. LEFT OUTER JOIN이므로 RIGHT에 있는 정보가 누락되더라도 추출되므로 위의 예에서는 USER 정보가 없는 ROLE 정보도 모두 리스트 됨을 알 수 있다.
조회 작업을 수행한 후, 조회 작업의 결과를 원하는 객체 형태로 전달받을 수 있다. 이는 여러 테이블을 Join할 경우 한 테이블에 매핑되는 Persistence 클래스가 아닌 composite 클래스로 리턴받고자 할 때 사용할 수 있다.
Relation 관계에 놓여 있는 두개의 테이블을 대상으로 QL(Inner Join)을 이용한 조회 결과를 특정 객체(예에선 User객체)형태로 전달받는다
StringBuffer qlBuf = new StringBuffer();
qlBuf.append("SELECT new User(user.userId as userId, ");
qlBuf.append(" user.userName as userName, user.password as password, ");
qlBuf.append(" role.roleName as roleName, ");
qlBuf.append(" user.department.deptName as deptName) ");
qlBuf.append("FROM User user join user.roles role ");
qlBuf.append("WHERE role.roleName = :condition");
Query query = em.createQuery(qlBuf.toString());
query.setParameter("condition", "Admin");
List userList = query.getResultList();
한가지 주의할 점은 new User(…)를 통해서 생성자를 호출하고 있는데 이 생성자가 User 클래스에 정의되어 있어야 한다. 또한 리턴된 결과값에서 각각의 attribute에 해당하는 값을 꺼낼 때에는 List에서 각 User 객체를 꺼낸 다음 getter 메소드를 사용한다.
User user1 = (User) userList.get(0);
user1.getUserName());
User user2 = (User) userList.get(1);
user2.getUserName());
Relation 관계에 놓여 있는 두개의 테이블을 대상으로 QL(Inner Join)을 이용한 조회 결과를 Map 형태로 전달받는다
StringBuffer qlBuf = new StringBuffer();
qlBuf.append("SELECT new Map(user.userId as userId, ");
qlBuf.append(" user.userName as userName, user.password as password, ");
qlBuf.append(" role.roleName as roleName, ");
qlBuf.append(" user.department.deptName as deptName) ");
qlBuf.append("FROM User user join user.roles role ");
qlBuf.append("WHERE role.roleName = :condition");
Query query = em.createQuery(qlBuf.toString());
query.setParameter("condition", "Admin");
List userList = query.getResultList();
위와 같이 정의할 경우 조회 결과는 Map의 List 형태가 된다. 이때 alias로 정의한 userId, userName, password, roleName, deptName이 Map의 Key 값이 된다. 따라서 다음과 같이 Map으로 정의된 Key 값을 통해 결과값을 조회할 수 있다.
List userList = query.getResultList();
Map user1 = (Map) userList.get(0);
user1.get("userId");
user1.get("userName");
...
Relation 관계에 놓여 있는 두개의 테이블을 대상으로 QL(Inner Join)을 이용한 조회 결과를 List 형태로 전달받는다
StringBuffer qlBuf = new StringBuffer();
qlBuf.append("SELECT new List(user.userId as userId, ");
qlBuf.append(" user.userName as userName, user.password as password, ");
qlBuf.append(" role.roleName as roleName, ");
qlBuf.append(" user.department.deptName as deptName) ");
qlBuf.append("FROM User user join user.roles role ");
qlBuf.append("WHERE role.roleName = :condition");
Query query = em.createQuery(qlBuf.toString());
query.setParameter("condition", "Admin");
List userList = query.getResultList();
위와 같이 정의할 경우 조회 결과는 List의 List 형태가 된다. List에서 결과값을 꺼낼 때에는 정의된 순서에 따르면 된다.
List userList = query.getResultList();
List user1 = (List) userList.get(0);
user1.get(1); //userId
user1.get(2); //userName
...
Entity 클래스 파일 내에 Annotation으로 정의한 QL문의 name을 입력하여 실행시킬 수 있다.
Query qlQuery = em.createNamedQuery("findDeptList");
qlQuery.setParameter("condition", "%%");
List deptList = qlQuery.getResultList();
위와 같이 createNamedQuery() 메소드에 query name을 넘겨주면 이 이름에 맞는 QL문을 찾아서 실행하게 된다. 다음은 findDeptList가 담겨있는 Department Entity 클래스 소스 일부이다.
@Entity
@NamedQuery(name = "findDeptList",
query = "FROM Department department WHERE department.deptName like :condition ORDER BY department.deptName")
public class Department implements Serializable {
...
}
Paging 처리는 한 페이지에 보여줘야 할 조회 목록에 제한을 둠으로써 DB 또는 어플리케이션 메모리의 부하를 감소시키고자 하는데 목적이 있다. QL 수행시 페이징 처리된 조회 결과를 얻기 위한 방법에 대해 알아보도록 한다. 특정 테이블을 대상으로(예에서는 USER 테이블) QL을 이용한 조회 작업을 수행한다. 이때, 조회를 시작해야 하는 Row의 Number(FirstResult)와 조회 목록의 개수(MaxResult)를 정의함으로써, 페이징 처리가 가능해진다.
StringBuffer qlBuf = new StringBuffer();
qlBuf.append("FROM User user ");
Query query = em.createQuery(qlBuf.toString());
//첫번째로 조회해야 할 항목의 번호
query.setFirstResult(1);
//조회 항목의 전체 개수
query.setMaxResults(2);
List userList = query.getResultList();
위와 같이 정의할 경우 QL에서는 persistence.xml 파일에 정의된 hibernate.dialect 속성에 따라 각각의 DB에 맞는 SQL을 생성한다. 이는 Pagination을 할 때 모든 데이터를 읽은 후 해당 페이지에 속한 데이터 갯수를 결과값으로 전달하는 것이 아니라 조회해야 할 데이터 즉, 해당 페이지에 속한 갯수만큼의 데이터만 읽어오게 된다.
기본적으로 JPA를 이용한 CUD(Create, Update, Delete)를 할 때에는 기본 API를 사용하게 된다. (본메뉴얼 Basic CRUD 참고) 그러나 특이한 경우 QL을 통해 기본 CUD를 수행해야 하는 경우가 발생할 수 있다.
다음은 QL을 사용한 Insert문의 예이다.
StringBuffer ql = new StringBuffer();
ql.append("INSERT INTO Department (deptId,deptName) ");
ql.append("SELECT CONCAT(deptId,'UPD'), CONCAT(deptName,'UPD') ");
ql.append("FROM Department department ");
ql.append("WHERE deptId = :deptId");
Query query = em.createQuery(ql.toString());
query.setParameter("deptId", "Dept1");
query.executeUpdate();
위와 같이 작성할 경우 QL을 이용하여 신규 Department 정보를 등록할 수 있다.
다음은 QL을 사용한 Update문의 예이다.
StringBuffer ql = new StringBuffer();
ql.append("UPDATE Department department ");
ql.append("SET department.desc = :desc ");
ql.append("WHERE department.deptId = :deptId and department.deptName = :deptName ");
Query query = em.createQuery(ql.toString());
query.setParameter("desc", "Human Resource");
query.setParameter("deptId", "Dept1");
query.setParameter("deptName", "HRD");
query.executeUpdate();
위의 예는 QL을 사용하여 Department 정보를 수정한 것이며 Query의 setParameter() 메소드를 통해 인자값을 세팅하고 있다.
다음은 QL을 사용한 Delete문의 예이다.
StringBuffer ql = new StringBuffer();
ql.append("DELETE Department department ");
ql.append("WHERE department.deptId = :deptId ");
Query query = em.createQuery(ql.toString());
query.setParameter("deptId", "Dept1");
query.executeUpdate();
위의 예는 QL을 사용하여 Department 정보를 삭제한 것이며 Query의 setParameter() 메소드를 통해 인자값을 세팅하고 있다.
기본적으로 CRUD 작업을 할 때 JPA 기본 API를 사용하거나 QL을 이용하여 수행한다. 그러나 특정 DBMS에서 제공하는 기능을 사용할 수 있도록 하기 위해 Native SQL 사용을 지원한다.
entityManager.createNativeQuery() 메소드를 이용하여 Native SQL을 실행할 수 있다.
SQL을 통해 하나의 테이블을 대상으로 조회 작업을 수행할 수 있다.
StringBuffer qlBuf = new StringBuffer();
qlBuf.append("SELECT * ");
qlBuf.append("FROM DEPARTMENT ");
qlBuf.append("WHERE DEPT_NAME like :condition ");
qlBuf.append("ORDER BY DEPT_NAME");
Query query = em.createNativeQuery(qlBuf.toString(),Department.class);
query.setParameter("condition", "%%");
List deptList = query.getResultList();
위와 같이 정의된 SQL문을 통해 조회 조건에 맞는 Department 객체의 List가 리턴된다. WHERE절에서 ‘:‘을 사용하여 Named Paramenter를 통해 조회 조건을 완성할 수 있다. 조회 조건의 값은 Query의 setParameter() 메소드를 통해 지정해 주고 있다. 또한, createNativeQuery의 두번재 인자로 리턴받고자하는 Entity 클래스(Department.class)를 지정한 것을 확인할 수 있다.
Relation 관계에 놓여 있는 두개의 테이블을 대상으로 Native SQL(Inner Join)을 이용한 조회 작업을 수행할 수 있다.
StringBuffer qlBuf = new StringBuffer();
qlBuf.append("SELECT user.* ");
qlBuf.append("FROM USER user ");
qlBuf.append("join AUTHORITY auth on user.USER_ID = auth.USER_ID ");
qlBuf.append("join ROLE role on auth.ROLE_ID = role.ROLE_ID ");
qlBuf.append("WHERE role.ROLE_NAME = ?");
Query query = em.createNativeQuery(qlBuf.toString(),User.class);
query.setParameter(1, "Admin");
List userList = query.getResultList();
위의 코드와 같이 join 키워드를 사용하여 Inner Join을 수행할 수 있다. 또한, Relation 관계에 놓여 있는 두개의 테이블을 대상으로 Native SQL(Right Outer Join)을 이용한 조회 작업을 수행할 수 있다.
StringBuffer qlBuf = new StringBuffer();
qlBuf.append("SELECT distinct role.* ");
qlBuf.append("FROM USER user ");
qlBuf.append("right join AUTHORITY auth on user.USER_ID=auth.USER_ID ");
qlBuf.append("right join ROLE role on auth.ROLE_ID=role.ROLE_ID ");
qlBuf.append("ORDER BY role.ROLE_NAME ASC ");
Query query = em.createNativeQuery(qlBuf.toString(),Role.class);
List roleList = query.getResultList();
또한 Join하여 조회한 결과를 각각의 Join된 객체의 값으로 select 하기 위해서는 createNativeQuery의 두번째 인자에 @SqlResultSetMapping에 정의된 명을 기재하여 수행한다.
StringBuffer qlBuf = new StringBuffer();
qlBuf.append("SELECT distinct user.*, department.* ");
qlBuf.append("FROM USER user, DEPARTMENT department ");
qlBuf.append("WHERE user.DEPT_ID = department.DEPT_ID ");
qlBuf.append("AND department.DEPT_NAME = :condition1 ");
qlBuf.append("AND user.USER_NAME like :condition2 ");
Query query = em.createNativeQuery(qlBuf.toString(), "UserAndDept" ) ;
query.setParameter("condition1", "HRD");
query.setParameter("condition2", "%%");
List userList = query.getResultList();
위의 예를 보면 User Entity Class에 UserAndDept 라는 이름으로 리턴받고자 하는 Entity 클래스를 정의하고 있음을 알 수 있다.
@SqlResultSetMapping(name="UserAndDept",entities={@EntityResult(entityClass=User.class),
@EntityResult(entityClass=Department.class)
}
)
@Entity
public class User implements Serializable {
}
위의 예를 보면 User Entity 클래스에서 Annotation을 통해서 UserAndDept를 정의하고 있음을 알 수 있다. 또한, 각각의 추출은 아래와 같이 한다.
Object[] results = (Object[]) userList.get(0);
User user1 = (User)results[0];
Department dept1 = (Department)results[1];
Entity 클래스 파일 내에 Annotation으로 정의한 SQL문의 name을 입력하여 실행시킬 수 있다.
Query query = em.createNamedQuery("nativeFindDeptList");
query.setParameter("condition", "%%");
List deptList = query.getResultList();
위와 같이 createNamedQuery() 메소드에 query name을 넘겨주면 이 이름에 맞는 QL문을 찾아서 실행하게 된다. 다음은 nativeFindDeptList가 담겨있는 Department Entity 클래스 소스 일부이다.
@Entity
@NamedNativeQuery(name="nativeFindDeptList",
resultClass=Department.class,
query="SELECT * FROM DEPARTMENT department " +
"WHERE department.DEPT_Name like :condition "+
"ORDER BY department.DEPT_Name" )
public class Department implements Serializable {
...
}
위에서 볼 수 있듯이 QL의 NamedQuery과는 resultClass 를 명시적으로 기재한다는 점에서 차이가 있다.
Paging 처리는 한 페이지에 보여줘야 할 조회 목록에 제한을 둠으로써 DB 또는 어플리케이션 메모리의 부하를 감소시키고자 하는데 목적이 있다. Native SQL 수행시 페이징 처리된 조회 결과를 얻기 위한 방법에 대해 알아보도록 한다. 특정 테이블을 대상으로(예에서는 USER 테이블) Native SQL을 이용한 조회 작업을 수행한다. 이때, 조회를 시작해야 하는 Row의 Number(FirstResult)와 조회 목록의 개수(MaxResult)를 정의함으로써, 페이징 처리가 가능해진다.
StringBuffer qlBuf = new StringBuffer();
qlBuf.append("SELECT * ");
qlBuf.append("FROM User ");
Query query = em.createNativeQuery(qlBuf.toString(),User.class);
query.setFirstResult(1);
query.setMaxResults(2);
List userList = query.getResultList();
위와 같이 정의할 경우 QL에서는 persistence.xml 파일에 정의된 hibernate.dialect 속성에 따라 각각의 DB에 맞는 SQL을 생성한다. 이는 Pagination을 할 때 모든 데이터를 읽은 후 해당 페이지에 속한 데이터 갯수를 결과값으로 전달하는 것이 아니라 조회해야 할 데이터 즉, 해당 페이지에 속한 갯수만큼의 데이터만 읽어오게 된다.
해당 DB에 생성한 Function을 이용하여 Native SQL을 실행하고 결과를 확인할 수 있다.
StringBuffer qlBuf = new StringBuffer();
qlBuf.append("SELECT * FROM USER_TBL ");
qlBuf.append("WHERE salary > FIND_USER(:condition)");
Query query = em.createNativeQuery(qlBuf.toString(),User.class);
query.setParameter("condition", "User1");
List userList = query.getResultList();
위의 예에서 보면 FIND_USER라는 함수를 호출하여 WHERE문에서 비교를 수행하는 것을 알 수 있다. Procedure의 경우는 입력/출력 인자 처리를 어찌 해야하는지에 대한 확인이 불가능해서 예제로 설명하지 못했다.
동시에 동일한 데이터에 접근할 때에 데이터에 대한 접근을 제어하기 위해 Optimistic Locking을 지원한다. 한편 Hibernate의 Native API를 통해서는 지원 가능한 Pessimistic Locking 은 JPA2.0 버전에 정의될 예정이다.
@Test
public void testUpdateUserWithoutOptimisticLocking() throws Exception {
// 1. 테스트를 위한 신규 데이터를 입력
newTransaction();
addDepartmentUserAtOnce();
closeTransaction();
// 2. 동일한 식별자를 이용하여 User 정보를 두번 조회
newTransaction();
User fstUser = (User) em.find(User.class,"User1");
User scdUser = (User) em.find(User.class,"User1");
closeTransaction();
// 3. Detached 상태에서의 변경처리
fstUser.setUserName("First : Kim");
// 4. 별도의 트랜잭션으로 변경처리
newTransaction();
scdUser.setUserName("Second : Kim");
closeTransaction();
// 5. 3에서 작업한 내용이 반영되어 변경.
newTransaction();
em.merge(fstUser);
closeTransaction();
}
위에서 제시한 로직에 대해 자세히 살펴보자.
결론적으로 보면, userId가 “User1”인 User의 userName은 “First : Kim”이 되어 앞서 scdUser에서 요청했던 수정 작업은 무시된 것이다. 이러한 현상을 Lost Update라고 하며, 이를 해결하기 위한 방법은 3가지가 있다.
JPA에서는 Versioning 기반의 Automatic Optimistic Locking을 통해 First Commit Wins 전략을 취할 수 있도록 지원한다. JPA에서 Optimistic Locking을 수행하기 위해서는 해당 테이블에 Version을 추가해야 한다. 그러한 경우 해당 테이블과 매핑된 객체를 로드할 때 Version 정보도 함께 로드되고 객체 수정시 테이블의 현재 값과 비교하여 처리 여부를 결정하게 된다.
@Test
public void testUpdateDepartmentWithOptimisticLocking() throws Exception {
// 1. 테스트를 위한 신규 데이터를 입력
newTransaction();
addDepartmentUserAtOnce();
closeTransaction();
// 2. Department 정보를 두번 조회
newTransaction();
Department fstDepartment = (Department) em.find(Department.class,"Dept1");
assertEquals("fail to check a version of department.", 0, fstDepartment.getVersion());
Department scdDepartment = (Department) em.find(Department.class,"Dept1");
closeTransaction();
// 3. 두번째 조회한 Department 정보에 다른 deptName을 셋팅하여 DB에 반영
fstDepartment.setDeptName("First : Dept.");
// 4. 첫번째 조회한 Department 정보에 대해 merge() 메소드를 호출
newTransaction();
scdDepartment.setDeptName("Second : Dept.");
closeTransaction();
// 5. 세번째 트랜잭션에서의 수정으로 인해 DEPARTMENT_VERSION이 이미 변경되었기 때문에
// StaleObjectStateException 발생이 예상
newTransaction();
try {
em.merge(fstDepartment);
closeTransaction();
} catch (Exception e) {
e.printStackTrace();
assertTrue("fail to throw StaleObjectStateException.",e instanceof StaleObjectStateException);
}
}
위와같이 다음의 testUpdateDepartmentWithOptimisticLocking() 메소드를 수행하였을 때 첫번째 수정 작업은 성공적으로 이루어지나 두번째 수정 작업에 대해서는 #6번 코드에서처럼 StaleObjectStateException이 throw될 것이다. 이를 위한 entity 클래스의 설정의 일부분은 다음과 같다.
@Entity
@Table(name="DEPARTMENT")
public class Department {
private static final long serialVersionUID = 1L;
@Id
@Column(name = "DEPT_ID", length = 10)
private String deptId;
@Version
@Column(name = "DEPT_VERSION")
private int version;
...
}
위에서 보는 것 같이 DEPT_VERSION이라는 컬럼을 추가하여 버전관리를 하게 함으로써 Optimistic Locking처리를 할 수 있다.
입력 인자로 전달된 객체를 정의된 테이블로 매핑시켜 데이터 액세스 처리를 수행해야 하는데 JPA에서는 이로 인해 발생 가능한 성능 이슈를 개선하기 위해 Cache를 활용한다. 현재 표준버전에서는 1 level Cache 만을 정의하고 있다. JPA 2.0에서는 2level Cache 정의 추가됨.
Entity Manager 내부에 정의된 Cache로, 트랜잭션의 시작과 종료 사이에서 사용되며 한 트랜잭션 내에서 읽혀진 객체들을 보관하는 역할을 수행한다. JPA 구현체는 하나의 트랜잭션 내에서 동일한 객체를 한 번 이상 Loading할 경우 2번째부터는 Cache로부터 해당 객체를 추출하고 또한, 한 트랜잭션 범위 내에서 객체의 속성 변경시 변경 사항은 트랜잭션 종료시에 자동적으로 DB에 반영하도록 한다. 즉, 하나의 트랜잭션 내에서 동일한 객체에 대한 재조회가 이루어지는 경우 Cache를 이용함으로써 DB 접근 횟수를 줄여주기 때문에 어플리케이션 성능 향상에 도움이 되는 것이다.
@Test
public void testFindUser() throws Exception {
newTransaction();
SetUpInitCacheData.initializeData(em);
User user = (User) em.find(User.class, "User1");
Set roles = (Set)user.getRoles();
roles.iterator();
// 1. Read from Cache
user = (User) em.find(User.class, "User1");
roles = (Set)user.getRoles();
roles.iterator();
closeTransaction();
}
위와 같이 작성할 경우 동일한 트랜잭션 내에서 SetUpInitCacheData.initializeData(em)을 통해 persist된 Persistence 객체는 1LC에 저장되므로 다음에 #1번 코드에서처럼 동일한 Persistence 객체 조회시 DB에 재접근하지 않고도, Cache를 통해 조회된다.
2 Level Cache에 대한 것은 JPA 2.0에서 정의하고 있어서 여기서는 JPA 구현체로 쓰이는 Hibernate에서 지원하는 방법으로 가이드를 하고자 한다. 2 Level Cache는 어플리케이션 단위의 Cache로, 어플리케이션 관점에서의 Cache 기능을 지원한다. 이는 여러 트랜잭션들을 통해 Load된 Persistence 객체를 Session Factory 레벨에서 저장하는 방법으로 처리된다. persistence.xml 파일 내에 hibernate.cache.use_second_level_cache, hibernate.cache.provider_class 등을 정의 하고, 2 Level Cache에 저장되어야 할 Persistence Class 매핑 파일의
<persistence-unit name="HSQLMCUnit" transaction-type="RESOURCE_LOCAL">
...
<properties>
<property name="hibernate.cache.use_second_level_cache" value="true"/>
<property name="hibernate.cache.provider_class" value="org.hibernate.cache.EhCacheProvider"/>
...
</properties>
</persistence-unit>
다음은 cache 속성이 READ_WRITE로 설정되어 있는 Entity 클래스의 일부이다.
// Department에 지정
@Cache(usage=CacheConcurrencyStrategy.READ_WRITE)
public class Department {
// Department와 User의 Join에 지정
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
@OneToMany(targetEntity=User.class, mappedBy="department"
,cascade={CascadeType.PERSIST, CascadeType.MERGE})
private Set<User> users ;
}
// User에 지정
@Entity
@Cache(usage=CacheConcurrencyStrategy.READ_WRITE)
public class User {
}
위에서 Department, User , Set
@Test
public void testFindDepartment() throws Exception {
// 데이터 입력.
newTransaction();
SetUpInitCacheData.initializeData(em);
closeTransaction();
//Hibernate 메소드를 이용하기 위해 Hibernate Session Factory 생성(Entity Manager 로부터 얻어냄)
newHibernateSessionFactory();
// 2Level Cache를 이용한 자료 추출(Hibernate의 메소드를 이용함)
newSession();
Department department = (Department) session.get(Department.class, "Dept1");
Set users = department.getUsers();
users.iterator();
closeSession();
// 2Level Cache를 이용한 자료 추출(Hibernate의 메소드를 이용함)
newSession();
department = (Department) session.get(Department.class, "Dept1");
users = department.getUsers();
users.iterator();
// Hibernate Session close
closeSession();
}
위의 예에서 두번째 Session.get()을 통해서는 DB 접근없이 2 Level Cache의 값을 가지고 오는 것을 알 수 있다.
ORM 서비스는 기본적으로 Entity간의 연관관계를 정의하고 정의된 연관관계를 가지고 관련 Entity를 추출하여 사용한다. 관련 Entity를 추출하는데 기본적으로 Lazy Loading이란 기법을 통해서 객체가 실제로 필요하기 전까지 SQL을 실행하지 않고 Proxy 객체로 리턴하도록 하고 한다. 그러나 이러한 Lazy Loading으로 처리하게 되면 Lazy Loading을 하지 않는 것에 대비하여 필요시점에 쿼리를 여러번 수행해야 하는 문제가 발생한다. 이런 문제를 해결하기 위한 여러가지 Fetch 전략이 존재하는데 Batch를 이용하여 데이터 조회, Sub-Query를 이용하여 데이터 조회, Join Fetch를 이용하여 데이터 한꺼번에 조회하는 방법이 있다. 하지만 이 서비스는 JPA표준이 아닌 구현체인 Hibernate에 정의된 사항이다.
Entity 클래스에 BatchSize를 지정할 경우 지정한 개수만큼 해당 객체를 로딩하는 방식으로 쿼리 실행 회수가 n / batch size + 1로 감소한다. 다음은 batch-size 설정 예인 Department 클래스 파일의 일부이다.
@Entity
public class Department implements Serializable {
...
@org.hibernate.annotations.BatchSize(size=2)
private Set<User> users ;
...
}
위에서 Department에 속한 Set
qlBuf.append("FROM Department");
Query query = em.createQuery(qlBuf.toString());
List deptList = query.getResultList();
assertEquals("fail to match the size of department list.", 3, deptList.size());
for (int i = 0; i < deptList.size(); i++) {
Department department = (Department) deptList.get(i);
if (i == 0) {
assertEquals("fail to match a department name.", "HRD", department.getDeptName());
Set users = department.getUsers();
assertEquals("fail to match the size of user list.", 2, users.size());
} else if (i == 1) {
assertEquals("fail to match a department name.", "PD", department.getDeptName());
Set users = department.getUsers();
assertEquals("fail to match the size of user list.", 1, users.size());
...
위의 Config와 Sample에 의해서 query.getResultList()에 의한 것과 department.getUsers() 호출 시 자동 생성되는 SQL Query는 다음과 같다.
SELECT ... FROM USER user0_
SELECT ...
FROM USER users0_
WHERE users0_.DEPT_ID IN (?, ?)
위에 where 절의 [in (?,?)]에서의 ?의 숫자가 BatchSize이다. 위에서 2로 설정했기에 두개 지정되어 조회된다.
Entity 클래스에 FetchMode를 SUBSELECT로 지정할 경우 Sub Query 형태의 SELECT 문이 수행되며 한번에 모두 로딩하게 된다. 다음은 SUBSELECT 설정 예인 User 클래스 파일의 일부이다.
@Entity
public class User implements Serializable {
...
@org.hibernate.annotations.Fetch(org.hibernate.annotations.FetchMode.SUBSELECT)
private Set<Role> roles = new HashSet(0);
...
}
위에서 User에 속한 Set<Role> 을 추출할 때 적용되는 FetchMode를 SUBSELECT 로 지정하였다.
qlBuf.append("FROM User");
Query query = em.createQuery(qlBuf.toString());
List userList = query.getResultList();
assertEquals("fail to match the size of user list.", 3, userList.size());
for (int i = 0; i < userList.size(); i++) {
User user = (User) userList.get(i);
if (i == 0) {
assertEquals("fail to match a user name.", "kim" , user.getUserName());
assertEquals("fail to match a user password.", "kim123" , user.getPassword());
Set roles = user.getRoles();
assertEquals("fail to match the size of role list.", 2 , roles.size());
} else if (i == 1) {
assertEquals("fail to match a user name.", "lee" , user.getUserName());
assertEquals("fail to match a user password.", "lee123" , user.getPassword());
...
위의 Config와 Sample에 의해서 query.getResultList() 호출 시와 user.getRoles() 호출 시 자동 생성되는 SQL Query는 다음과 같다.
SELECT ... FROM USER user0_
SELECT ...
FROM AUTHORITY roles0_ LEFT OUTER JOIN ROLE role1_ ON roles0_.ROLE_ID=role1_.ROLE_ID
WHERE roles0_.USER_ID IN (SELECT user0_.USER_ID FROM USER user0_)
위에 where절의 [in (select user0_.USER_ID from USER user0_)]를 보면 Sub Query 형태로 해당하는 모든 User에 대해서 모든 Roles 정보를 추출하는 것을 확인 할 수 있다.
Entity 클래스에서의 별도 설정없이 QL 수행시 join fetch 를 기재함으로써 한번에 연관된 자식 엔티티를 모두 추출하여 사용한다.
qlBuf.append("SELECT user ");
// JOIN FETCH 를 이용함.
qlBuf.append("FROM User user join fetch user.roles role ");
qlBuf.append("WHERE role.roleName = ?");
Query query = em.createQuery(qlBuf.toString());
query.setParameter(1, "Admin");
List userList = query.getResultList();
assertEquals("fail to match the size of user list.", 2, userList.size());
for (int i = 0; i < userList.size(); i++) {
User user = (User) userList.get(i);
if (i == 0) {
assertEquals("fail to match a user name.", "kim",user.getUserName());
assertEquals("fail to match a user password.", "kim123",user.getPassword());
Set roles = user.getRoles();
assertEquals("fail to match the size of role list.", 1, roles.size());
} else if (i == 1) {
assertEquals("fail to match a user name.", "lee",user.getUserName());
assertEquals("fail to match a user password.", "lee123",user.getPassword());
...
위의 예를 보면 [FROM User user join fetch user.roles role]에서 join fetch 라는 키워드를 쓴 것을 확인할 수 있다. 이것에 의한 생성된 SQL은 다음과 같다.
SELECT ...
FROM USER user0_ INNER JOIN AUTHORITY roles1_ ON user0_.USER_ID=roles1_.USER_ID
INNER JOIN ROLE role2_ ON roles1_.ROLE_ID=role2_.ROLE_ID
WHERE role2_.ROLE_NAME=?
위에 SQL은 query.getResultList()에 의해서 실행되는 SQL로 JOIN에 관련된 ROLE정보를 모두 추출하는 것을 확인할 수 있다.
Spring에서는 JPA 기반에서 DAO 클래스를 쉽게 구현할 수 있도록 하기 위해 JdbcTemplate,HibernateTemplate등처럼 JpaTemplate을 제공한다. 하지만 JPA에 있어서는 Entity Manager의 Method를 직접 이용하는 것(plain JPA)에 대한 것도 가이드한다. 이에 두가지 방법에 대한 설정 및 사용방법에 대해서 설명하고자 한다. Spring JPA
Spring 기반하에서 JPA를 쓰고자 할 때 필요한 설정은 persistence.xml과 ApplicationContext 파일 설정이 필요하다.
<persistence-unit name="HSQLMUnit" transaction-type="RESOURCE_LOCAL">
// 구현체는 Hibernate
<provider>org.hibernate.ejb.HibernatePersistence</provider>
// Entity Class List
<class>egovframework.sample.model.bidirection.User</class>
<class>egovframework.sample.model.bidirection.Role</class>
<class>egovframework.sample.model.bidirection.Department</class>
<exclude-unlisted-classes/>
<properties>
// DBMS별 다른 설정 여기는 HSQL 설정.
<property name="hibernate.dialect" value="org.hibernate.dialect.HSQLDialect"/>
<property name="hibernate.show_sql" value="true"/>
<property name="hibernate.format_sql" value="true"/>
<property name="hibernate.hbm2ddl.auto" value="create-drop"/>
</properties>
</persistence-unit>
위에서 Entity Class List와 그에 따르는 <exclude-unlisted-classes/>는 프로젝트내에 있는 엔티티 클래스중에 리스트하는 것만을 엔티티로 인식하도록 설정하는 것이고 dialect설정은 DBMS별 별도 설정이다. 위의 예에서는 HSQL 설정.
// 1.Transation Manager 설정
<bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager">
<property name="entityManagerFactory" ref="entityManagerFactory"/>
</bean>
// 2.Entity Manager Factory 설정
<bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
<property name="persistenceUnitName" value="HSQLMUnit"/>
<property name="persistenceXmlLocation" value="classpath:META-INF/persistHSQLMemDB.xml"/>
<property name="dataSource" ref="dataSource"/>
</bean>
// 3.DataSource 설정
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
<property name="driverClassName" value="net.sf.log4jdbc.DriverSpy"/>
<property name="url" value="jdbc:log4jdbc:hsqldb:mem:testdb"/>
<property name="username" value="sa"/>
<property name="password" value=""/>
<property name="defaultAutoCommit" value="false"/>
</bean>
// 4.JPA Annotation 사용 설정
<bean class="org.springframework.orm.jpa.support.PersistenceAnnotationBeanPostProcessor"/>
// 5.Annotation 사용 설정
<context:component-scan base-package="egovframework"/>
// 6.Annotation 기반의 Transaction 활성화 설정
<tx:annotation-driven />
위의 예를 살펴보면 1.Transation Manager 설정, 2.Entity Manager Factory 설정, 3.DataSource 설정, 4.JPA Annotation 사용 설정, 5.Annotation 사용 설정, 6.Annotation 기반의 Transaction 활성화 설정 으로 구분되어 설정되어 있고 1~4까지가 JPA를 위한 설정이다. 각자 쓰고자 할때 변경이 필요한 부분은 2,3,5 내역으로 2번 항목은 persistence.xml 파일 위치와 persistenceUnitName 설정, 3번 항목은 DBMS연결을 위한 DataSource 설정, 5번 항목은 package 설정이다.
Spring에서 정의한 JpaDaoSupport를 상속받아 getJpaTemplate()를 통해서 Entity Method 등을 호출 작업할 수 있다.
public class UserDAO extends JpaDaoSupport {
// Application Context 에서 설정한 Entity Manager Factory 명을 지정하여 부모의 EntityManagerFactory를 설정한다.
@Resource(name="entityManagerFactory")
public void setEMF(EntityManagerFactory entityManagerFactory) {
super.setEntityManagerFactory(entityManagerFactory);
}
// getTemplate()에 의한 입력
public void createUser(User user) throws Exception {
this.getJpaTemplate().persist(user);
}
// getTemplate()에 의한 조회
public User findUser(String userId) throws Exception {
return (User) this.getJpaTemplate().find(User.class, userId);
}
// getTemplate()에 의한 query .. find method 지원됨
public List findUserListAll() throws Exception {
return this.getJpaTemplate().find("FROM User user ORDER BY user.userName");
}
// getTemplate()에 의한 삭제
public void removeUser(User user) throws Exception {
this.getJpaTemplate().remove(this.getJpaTemplate().getReference(User.class, user.getUserId()));
}
// getTemplate()에 의한 수정
public void updateUser(User user) throws Exception {
this.getJpaTemplate().merge(user);
}
}
위의 예를 보면 JpaDaoSupport 를 상속받아서 this.getJpaTemplate().method()를 통해서 기능을 구현하였다.
@Entity
public class User implements Serializable {
private static final long serialVersionUID = -8077677670915867738L;
@Id
@Column(name = "USER_ID", length=10)
private String userId;
@Column(name = "USER_NAME", length=20)
private String userName;
@Column(length=20)
private String password;
...
}
위의 예제는 DAO 클래스에서 쓰인 User Entity Class 소스의 일부이다.
JPA에서 정의한 Entity Manager의 Entity Method를 호출 작업할 수 있다. Entity Manager를 통해 작업함으로써 Spring 환경하에서 Spring에 대한 의존성을 최소화 할 수 있다.
public class RoleDAO {
// Application Context 설정의 4.JPA Annotation 사용 설정에 의해서 정의가능한 것으로 Annotation기반으로 Entity Manager를 지정한다.
@PersistenceContext
private EntityManager em;
// EntityManager를 통한 입력
public void createRole(Role role) throws Exception {
em.persist(role);
}
// EntityManager를 통한 조회
public Role findRole(String roleId) throws Exception {
return (Role) em.find(Role.class, roleId);
}
// EntityManager를 통한 Query
public List findRoleListAll() throws Exception {
Query query = em.createQuery("FROM Role role ORDER BY role.roleName");
return query.getResultList();
}
// EntityManager를 통한 삭제
public void removeRole(Role role) throws Exception {
em.remove(em.getReference(Role.class, role.getRoleId()));
}
// EntityManager를 통한 수정
public void updateRole(Role role) throws Exception {
em.merge(role);
}
}
위의 예를 보면 Entity Manager의 메소드를 통해서 기능을 구현하였다
@Entity
public class Role implements Serializable {
private static final long serialVersionUID = 1042037005623082102L;
@Id
@Column(name = "ROLE_ID", length=10)
private String roleId;
@Column(name = "ROLE_NAME", length=20)
private String roleName;
@Column(name = "DESC" , length=50)
private String desc;
...
}
위의 예제는 DAO 클래스에서 쓰인 Role Entity Class 소스의 일부이다.
JPA는 persistence.xml 파일을 기반으로 동작하며, 이 파일은 실행 속성을 포함하고 여러 개의 persistence-unit을 정의할 수 있다. persistence.xml은 JPA 설정의 핵심 요소로, 상위에
JPA는 실행 속성을 포함하고 있는 persistence.xml을 기반으로 하여 동작하도록 구성되어 있다. persistence.xml 파일의 주요 구성 요소와 속성 정의 방법에 대해 살펴보기로 한다. 먼저, persistence.xml 파일은 가장 상위에 <persistence> 태그를 포함하고 있으며 <persistence> 태그 내에 여러개의 <persistence-unit>를 포함할 수 있다.
Persistence Unit에 포함하고 있는 주요한 엔티티들은 다음과 같다.
| element 명 | 설 명 |
|---|---|
| provider | Entity Manager를 지원하는 Provider 클래스 |
| mapping-file | 매핑정보 파일 |
| class | Entity 클래스 리스트, @Entity, @Embeddable or @MappedSuperclass 를 포함하고 있는 클래스 |
| exclude-unlisted-classes | class 에서 정의하지 않은 것은 제외 |
| properties | JPA 구현체 프로퍼티 리스트 |
상세한 정보는 스키마 참조 아래는 위의 항목을 포함하고 있는 설정파일 예입니다.
<persistence-unit name="HSQLMUnit" transaction-type="RESOURCE_LOCAL">
<provider>org.hibernate.ejb.HibernatePersistence</provider>
<class>egovframework.sample.model.bidirection.User</class>
<exclude-unlisted-classes/>
<properties>
<property name="hibernate.connection.driver_class" value="net.sf.log4jdbc.DriverSpy"/>
...
Properties 아래에 정의되는 Vendor별 설정 정보중에 Hibernate의 설정정보에 대해서 설명한다. 좀더 자세한 사항은 Hibernate사이트를 참조한다.
아래의 속성들을 통해 Hibernate는 특정 DB에 접근하여 데이터 액세스 처리가 가능하다.
| 속 성 명 | 설 명 |
|---|---|
| hibernate.connection.driver_class | 접근 대상이 되는 DB의 Driver 클래스명을 정의하기 위한 속성 |
| hibernate.connection.url | 접근 대상이 되는 DB의 URL을 정의하기 위한 속성 |
| hibernate.connection.username | DB에 접근할 때 사용할 사용자명을 정의하기 위한 속성 |
| hibernate.connection.password | DB에 접근할 때 사용할 패스워드를 정의하기 위한 속성 |
다음은 위에서 언급한 속성들을 포함하고 있는 persistence.xml 파일의 일부이다
<property name="hibernate.connection.driver_class" value="net.sf.log4jdbc.DriverSpy"/>
<property name="hibernate.connection.url" value="jdbc:log4jdbc:hsqldb:mem:testdb"/>
<property name="hibernate.connection.username" value="sa"/>
아래의 속성들을 통해 Hibernate는 특정 DB에 접근하여 데이터 액세스 처리가 가능하다.
| 속 성 명 | 설 명 |
|---|---|
| hibernate.dialect | Hibernate 기반 개발시 DB에 특화된 SQL을 구성하지 않더라도 DB에 따라 알맞은 SQL을 생성할 수 있다. 이를 위해서 Dialect 클래스를 사용한다. hibernate.dialect는 Dialect 클래스명을 정의하기 위한 속성 |
| hibernate.default_schema | Hibernate에서 SQL을 생성할 때 특정 테이블에 대해 별도 정의된 Schema가 없는 경우 적용할 DB Schema 명을 정의하기 위한 속성 |
| hibernate.default_catalog | Hibernate에서 SQL을 생성할 때 특정 테이블에 대해 별도 정의된 Catalog가 없는 경우 적용할 DB Catalog 명을 정의하기 위한 속성 |
다음은 위에서 언급한 속성들을 포함하고 있는 persistence.xml 파일의 일부이다
<property name="hibernate.dialect" value="org.hibernate.dialect.HSQLDialect"/>
다음은 Hibernate에서 제공하는 주요 Dialect 클래스 목록이다
| DB 종류 | Dialect 클래스 |
|---|---|
| Oracle 10g | org.hibernate.dialect.Oracle10gDialect |
| Oracle 9i/10i | org.hibernate.dialect.Oracle9iDialect |
| Oracle (모든 버전) | org.hibernate.dialect.OracleDialect |
| MySQL 5.x | org.hibernate.dialect.MySQL5Dialect |
| MySQL 4.x, 3.x | org.hibernate.dialect.MySQLDialect |
| DB2 | org.hibernate.dialect.DB2Dialect |
| Sybase 11.9.2 | org.hibernate.dialect.Sybase11Dialect |
| Sybase Anywhere | org.hibernate.dialect.SybaseAnywhereDialect |
아래의 속성들을 통해 Hibernate는 Cache 기능을 지원한다.
| 속 성 명 | 설 명 |
|---|---|
| hibernate.cache.provider_class | Cache 기능을 구현하고 있는 구현체의 클래스명을 정의하기 위한 속성 |
| hibernate.cache.use_second_level_cache | 2nd Level Cache를 적용할지 여부를 정의하기 위한 속성 (true/false) |
| hibernate.cache.use_query_cache | Hibernate Query를 Caching할지 여부를 정의하기 위한 속성 (true/false) |
다음은 위에서 언급한 속성들을 포함하고 있는 persistence.xml 파일의 일부이다
<property name="hibernate.cache.use_second_level_cache" value="true"/>
<property name="hibernate.cache.use_query_cache" value="true"/>
<property name="hibernate.cache.provider_class" value="org.hibernate.cache.EhCacheProvider"/>
아래의 속성들을 통해 Hibernate는 좀더 자세한 Logging 기능을 지원한다.
| 속 성 명 | 설 명 |
|---|---|
| hibernate.show_sql | Hibernate을 통해 생성된 SQL을 콘솔에 남길 것인지 여부를 정의하는 속성 (true/false) |
| hibernate.format_sql | hibernate.show_sql=true인 경우 해당 SQL문의 포맷을 정돈하여 콘솔에 남길 것인지 여부를 정의하는 속성 (true/false) |
| hibernate.use_sql_comments | Hibernate을 통해 생성된 SQL을 콘솔에 남길 때 Comments도 같이 남길 것인지 여부를 정의하는 속성 (true/false) |
다음은 위에서 언급한 속성들을 포함하고 있는 persistence.xml 파일의 일부이다
<property name="hibernate.show_sql" value="true"/>
<property name="hibernate.format_sql" value="true"/>
| 속 성 명 | 설 명 |
|---|---|
| hibernate.hbm2ddl.auto | DDL을 자동으로 검증,생성 또는 수정할지 여부를 정의하기 위한 속성 (validate/ update/ create/ create-drop) |
| hibernate.jdbc.batch_size | Hibernate는 일반적으로 실행해야 할 SQL들에 대해 일괄적으로 batch 처리를 수행하는데 이 때 batch로 처리할 SQL의 개수를 정의하기 위한 속성 |
다음은 위에서 언급한 속성들을 포함하고 있는 persistence.xml 파일의 일부이다
<property name="hibernate.hbm2ddl.auto" value="create-drop"/>
<property name="hibernate.jdbc.batch_size" value="10" />
트랜잭션 서비스는 Spring 트랜잭션 서비스를 채택하여 가이드한다. 트랜잭션 서비스에는 여러가지가 있지만 여기서는 DataSource Transaction Service, JTA Transaction Service, JPA Transaction Service에 대해서 설명하고 트랜잭션 활용에 대해서는 설정 및 Annotation을 통해서 활용할 수 있는 Declaration Transaction Management와 프로그램에서 직접 API를 호출하여 쓸 수 있도록 하는 Programmatic Transaction Management 두가지에 대해서 설명한다.
DataSource를 사용하여 Local Transaction을 관리할 수 있다. 아래에서 예를 들어서 설정 방법과 사용법을 설명한다.
Configuration
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
<property name="driverClassName" value="com.mysql.jdbc.Driver"/>
<property name="url" value="dbc:mysql://db2:1621/rte"/>
<property name="username" value="rte"/>
<property name="password" value="xxx"/>
<property name="defaultAutoCommit" value="false"/>
</bean>
| PROPERTIES | 설 명 |
|---|---|
| driverClassName | jdbc driver |
| url | db url |
| username | 사용자명 |
| password | 패스워드 |
| defaultAutoCommit | 자동commit 설정 |
위의 설정을 보면 transactionManager의 property로 dataSource를 지정하고 그에 필요한 driver정보,Url정보등을 지정한 것을 확인 할 수 있다. 설정한 dataSource 기반하에서 트랜잭션 서비스를 제공한다. 사이트 환경에 맞추어 driverClassName,url,username,password는 변경해서 적용한다.
Sample Source
@Resource(name="transactionManager")
PlatformTransactionManager transactionManager;
...
TransactionStatus txStatus = transactionManager.getTransaction(txDefinition);
위의 예와 같이 transactionManager을 활용할 수 있다.
자바 트랜잭션 API(Java Transaction API, JTA)는 XA 리소스(예, 데이터베이스) 간의 분산 트랜잭션을 처리하는 자바 API이다.
Java에서 제공되는 대부분의 API와 마찬가지로, JTA는 실제 구현은 다르지만 어플리케이션이 공통적으로 사용할 수 있는 하나의 인터페이스를 제공한다. 이 말은 트랜잭션 처리가 필요한 어플리케이션이 (API의 사용 방식 그대로만 사용한다면) 특정 벤더의 트랜잭션 매니저에 의존할 필요가 없음을 의미한다.
그에따라, JTA는 일반적으로 단일 데이터베이스 또는 여러 개의 데이터베이스를 이용할 경우 트랜잭션을 제어하기 위한 목적으로 사용된다.
JTA를 이용하여 Global Transation관리를 할 수 있도록 지원하는 것으로 아래에서 예를 들어서 설정 방법을 설명한다. 사용법은 DataSource Transaction Service와 동일하다.
Configuration
<tx:jta-transaction-manager/>
<jee:jndi-lookup id="dataSource" jndi-name="dbmsXADS" resource-ref="true">
<jee:environment>
java.naming.factory.initial=weblogic.jndi.WLInitialContextFactory
java.naming.provider.url=t3://was:7002
</jee:environment>
</jee:jndi-lookup>
위의 설정예에서 jndi-name 과 java.naming.factory.initial,java.naming.provider.url은 사이트 환경에 맞추어 변경해야 한다. DataSource Transaction Service와는 달리 transationManager에 대해서 따로 bean 정의하지 않아도 된다.
JPA Transaction 서비스는 JPA EntityManagerFactory를 이용하여 트랜잭션을 관리한다. JpaTransactionManager는 EntityManagerFactory에 의존성을 가지고 있으므로 반드시 EntityManagerFactory 설정과 함께 정의되어야 한다. 아래에서 예를 들어서 설정 방법을 설명한다. 사용법은 DataSource Transaction Service와 동일하다.
Configuration
<bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager">
<property name="entityManagerFactory" ref="entityManagerFactory"/>
</bean>
<bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
<property name="persistenceUnitName" value="OraUnit"/>
<property name="persistenceXmlLocation" value="classpath:META-INF/persistence.xml"/>
<property name="dataSource" ref="dataSource"/>
</bean>
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
<property name="driverClassName" value="com.mysql.jdbc.Driver"/>
<property name="url" value="dbc:mysql://db2:1621/rte"/>
<property name="username" value="rte"/>
<property name="password" value="xxx"/>
<property name="defaultAutoCommit" value="false"/>
</bean>
| PROPERTIES | 설 명 |
|---|---|
| driverClassName | jdbc driver |
| url | db url |
| username | 사용자명 |
| password | 패스워드 |
| defaultAutoCommit | 자동commit 설정 |
| persistenceUnitName | persistenceUnitName |
| persistenceXmlLocation | XML 위치 |
| dataSource | 데이타소스 |
위의 설정을 보면 transactionManager의 property로 entiyManagerFactory로 지정하고 entityManagerFactory의 property로 dataSource를 지정하고 그에 필요한 driver정보,Url정보등을 지정한 것을 확인 할 수 있다. 설정한 dataSource 기반하에서 트랜잭션 서비스를 제공한다. 사이트 환경에 맞추어 driverClassName,url,username,password는 변경해서 적용한다. 또한 persistenceUnitName과 persistenceXmlLocation 정보를 지정하는 것을 알수 있다.
코드에서 직접적으로 Transaction 처리하지 않고, 선언적으로 Transaction을 관리할 수 있는데 Annotation을 이용한 Transaction 관리, XML 정의를 이용한 Transaction 관리를 지원한다.
Annotation 설정을 이용해서 Transaction을 관리할 수 있는데 아래에서 예를 들어서 설정 방법과 사용법을 설명한다.
<tx:annotation-driven transaction-manager="transactionManager" />
설정 XML에 위의 <tx:annotation-driven..>을 기재하면 설정된다. transactionManager는 TransactionManager 설정 참조
@Transactional
public void removeRole(Role role) throws Exception {
this.roleDAO.removeRole(role);
}
위의 예를 보면 @Transactional을 트랜잭션 처리하고자 하는 메소드위에 기재하여 트랜잭션 관리를 할 수 있다. @Transactional에 속성을 정의하여 쓸 수 있는데 속성 목록은 아래와 같다.
| 속 성 | 설 명 | 사 용 예 |
|---|---|---|
| isolation | Transaction의 isolation Level 정의하는 요소. 별도로 정의하지 않으면 DB의 Isolation Level을 따름. | @Transactional(isolation=Isolation.DEFAULT) |
| noRollbackFor | 정의된 Exception 목록에 대해서는 rollback을 수행하지 않음. | @Transactional(noRollbackFor=NoRoleBackTx.class) |
| noRollbackForClassName | Class 객체가 아닌 문자열을 이용하여 rollback을 수행하지 않아야 할 Exception 목록 정의 | @Transactional(noRollbackForClassName=“NoRoleBackTx”) |
| propagation | Transaction의 propagation 유형을 정의하기 위한 요소 | @Transactional(propagation=Propagation.REQUIRED) |
| readOnly | 해당 Transaction을 읽기 전용 모드로 처리 (Default = false) | @Transactional(readOnly = true) |
| rollbackFor | 정의된 Exception 목록에 대해서는 rollback 수행 | @Transactional(rollbackFor=RoleBackTx.class) |
| rollbackForClassName | Class 객체가 아닌 문자열을 이용하여 rollback을 수행해야 할 Exception 목록 정의 | @Transactional(rollbackForClassName=“RoleBackTx”) |
| timeout | 지정한 시간 내에 해당 메소드 수행이 완료되지 않은 경우 rollback 수행. -1일 경우 no timeout (Default = -1) | @Transactional(timeout=10) |
XML 정의 설정을 이용해서 Transaction을 관리할 수 있는데 아래에서 예를 들어서 설정 방법과 사용법을 설명한다.
<aop:config>
<aop:pointcut id="requiredTx" expression="execution(* egovframework.sample..impl.*Impl.*(..))"/>
<aop:advisor advice-ref="txAdvice" pointcut-ref="requiredTx" />
</aop:config>
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes>
<tx:method name="find*" read-only="true"/>
<tx:method name="createNoRBRole" no-rollback-for="NoRoleBackTx"/>
<tx:method name="createRBRole" rollback-for="RoleBackTx"/>
<tx:method name="create*"/>
</tx:attributes>
</tx:advice>
위의 설정을 보면 aop:pointcut를 이용하여 실행되어 Catch해야 하는 Method를 지정하고 tx:advice를 통해서 각각에 대한 룰을 정의하고 있다. 이렇게 정의하면 프로그램내에서는 별도의 트랜잭션 관련한 사항에 대해서 기술하지 않아도 트랜잭션관리가 된다. 위 샘플 XML에서와 같이 Transaction 관리를 위해 tx:advice 하위 태그인 tx:method에는 다음과 같은 상세 속성 정보를 부여할 수 있다. 관련 속성 정보는 아래와 같다.
| 속 성 | 설 명 | 사 용 예 |
|---|---|---|
| name | 메소드명 기술. 와일드카드 사용 가능함 | name=“find*” |
| isolation | Transaction의 isolation Level 정의하는 요소. 별도로 정의하지 않으면 DB의 Isolation Level을 따름. | isolation=“DEFAULT” |
| no-rollback-for | 정의된 Exception 목록에 대해서는 rollback을 수행하지 않음. | no-rollback-for=“NoRoleBackTx” |
| propagation | Transaction의 propagation 유형을 정의하기 위한 요소 | propagation=“REQUIRED” |
| read-only | 해당 Transaction을 읽기 전용 모드로 처리 (Default = false) | read-only=“true” |
| rollback-for | 정의된 Exception 목록에 대해서는 rollback 수행 | rollback-for=“RoleBackTx” |
| timeout | 지정한 시간 내에 해당 메소드 수행이 완료되지 않은 경우 rollback 수행. -1일 경우 no timeout (Default = -1) | timeout=“10” |
관련 속성별 가능 값 정보는 스키마 참조.
위에서 설명한 두가지 Transaction Management에 공통적으로 사용되는 항목은 Propagation 과 Isolation Level에 대한 설명을 하고자 한다.
트랜잭션의 전파 규칙을 설정하기 위해 사용한다.
| 속 성 명 | 설 명 |
|---|---|
| PROPAGATION_MADATORY | 반드시 Transaction 내에서 메소드가 실행되어야 한다. 없으면 예외발생 |
| PROPAGATION_NESTED | Transaction에 있는 경우, 기존 Transaction 내의 nested transaction 형태로 메소드를 실행하고, nested transaction 자체적으로 commit, rollback이 가능하다. Transaction이 없는 경우, PROPAGATION_REQUIRED 속성으로 행동한다. nested transaction 형태로 실행될 때는 수행되는 변경사항이 커밋이 되기 전에는 기존 Transaction에서 보이지 않는다. |
| PROPAGATION_NEVER | Manatory와 반대로 Transaction 없이 실행되어야 하며 Transaction이 있으면 예외를 발생시킨다. |
| PROPAGATION_NOT_SUPPORTED | Transaction 없이 메소드를 실행하며,기존의 Transaction이 있는 경우에는 이 Transaction을 호출된 메소드가 끝날 때까지 잠시 보류한다 |
| PROPAGATION_REQUIRED | 기존 Transaction이 있는 경우에는 기존 Transaction 내에서 실행하고, 기존 Transaction이 없는 경우에는 새로운 Transaction을 생성한다. |
| PROPAGATION_REQUIRED_NEW | 호출되는 메소드는 자신 만의 Transaction을 가지고 실행하고, 기존의 Transaction들은 보류된다 |
| PROPAGATION_SUPPORTS | 새로운 Transaction을 필요로 하지는 않지만, 기존의 Transaction이 있는 경우에는 Transaction 내에서 메소드를 실행한다. |
Transaction에서 일관성이 없는 데이터를 허용하도록 하는 수준이며, 여러 Transaction들이 다른 Transaction의 방해로부터 보호되는 정도를 나타낸다. 좀더 자세한 설명은 여기 참고.
| 속 성 명 | 설 명 |
|---|---|
| ISOLATION_DEFAULT | 개별적인 PlatformTransactionManager를 위한 격리 레벨 |
| ISOLATION_READ_COMMITTED | 이 격리수준을 사용하는 메소드는 commit 되지 않은 데이터를 읽을 수 없다. 쓰기 락은 다른 Transaction에 의해 이미 변경된 데이터는 얻을수 없다. 따라서 조회 중인 commit 되지 않은 데이터는 불가능하다. 대개의 데이터베이스에서의 디폴트로 지원하는 격리 수준이다. |
| ISOLATION_READ_UNCOMMITTED | 가장 낮은 Transaction 수준이다. 이 격리수준을 사용하는 메소드는 commit 되지 않은 데이터를 읽을 수 있다. 그러나 이 격리수준은 새로운 레코드가 추가되었는지 알수 없다. |
| ISOLATION_REPEATABLE_READ | ISOLATION_READ_COMMITED 보다는 다소 조금 더 엄격한 격리 수준이다. 이 격리 수준은 다른 Transaction이 새로운 데이터를 입력했다면, 새롭게 입력된 데이터를 조회할 수 있다는 것을 의미한다. |
| ISOLATION_SERIALIZABLE | 가장 높은 격리수준이다. 모든 Transaction(조회를 포함하여)은 각 라인이 실행될 때마다 기다려야 하기 때문에 매우 느리다. 이 격리수준을 사용하는 메소드는 데이터 상에 배타적 쓰기를 락을 얻음으로써 Transaction이 종료될 때까지 조회, 수정, 입력 데이터로부터 다른 Transaction의 처리를 막는다. 가장 많은 비용이 들지만 신뢰할만한 격리 수준을 제공하는 것이 가능하다. |
프로그램에서 직접 트랜잭션을 관리하고자 할 때 사용할 수 있는 방법에 대해서 설명하고자 한다. TransactionTemplate를 사용하는 방법과 Trnasaction Manager를 사용하는 방법 두가지가 있다.
TransactionTemplate를 정의하고 callback 메소드 정의를 이용하여 Transaction 관리를 할 수 있도록 하는 방법이다.
Configuration
<bean id="transactionTemplate" class="org.springframework.transaction.support.TransactionTemplate">
<property name="transactionManager" ref="transactionManager"/>
</bean>
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
| PROPERTIES | 설 명 |
|---|---|
| transactionManager | 트랜잭션매니저 |
| dataSource | 데이타소스 |
위의 설정에서 transactionTemplate를 정의하고 property로 transactionManager을 정의한다. Templeate를 이용한 샘플은 아래와 같다.
@Test
public void testInsertCommit() throws Exception {
transactionTemplate.execute(new TransactionCallbackWithoutResult() {
public void doInTransactionWithoutResult(TransactionStatus status) {
try {
Role role = new Role();
role.setRoleId("ROLE-001");
role.setRoleName("ROLE-001");
role.setRoleDesc(new Integer(1000));
roleService.createRole(role);
} catch (Exception e) {
status.setRollbackOnly();
}
}
});
Role retRole = roleService.findRole("ROLE-001");
assertEquals("roleName Compare OK",retRole.getRoleName(),"ROLE-001");
}
위의 예에서 transactionTemplate.execute에 TransactionCallbackWithoutResult를 정의하여 Transaction 관리를 하는 것을 확인할 수 있다.
Transaction Context에 의해 호출될 callback 메소드를 정의하고 이 메소드 내에 비즈니스 로직을 구현해주면 된다. 아래는 사용하는 방법의 예이다
this.transactionTemplate.execute(new TransactionCallbackWithoutResult() {
public void doInTransactionWithoutResult(TransactionStatus status) {
//... biz. logic ...
}
});
this.transactionTemplate.execute(new TransactionCallback() {
public Object doInTransaction(TransactionStatus status) {
//... biz. logic ...
}
});
callback 메소드 doInTransactionWithoutResult()는 Result 값이 없을 경우에 사용하고, Result 값이 존재하는 경우에는 doInTransaction()으로 사용하도록 한다. 또한, callback 메소드 내에서 입력 인자인 TransactionStatus 객체의 setRollbackOnly() 메소드를 호출함으로써 해당 Transaction을 rollback할 수 있다.
Transaction Manager를 직접 이용하는 방법이다.
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
| PROPERTIES | 설 명 |
|---|---|
| dataSource | 데이타소스 |
위의 설정에서 transactionManager을 정의한다.
@Test
public void testInsertRollback() throws Exception {
int prevCommitCount = roleService.getCommitCount();
int prevRollbackCount = roleService.getRollbackCount();
DefaultTransactionDefinition txDefinition = new DefaultTransactionDefinition();
txDefinition.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
TransactionStatus txStatus = transactionManager.getTransaction(txDefinition);
try {
Role role = new Role();
role.setRoleId(Thread.currentThread().getName() + "-roleId");
role.setRoleName(Thread.currentThread().getName() + "-roleName");
role.setRoleDesc(new Integer(1000));
roleService.createRole(role);
roleService.createRole(role);
transactionManager.commit(txStatus);
}
catch (Exception e) {
transactionManager.rollback(txStatus);
} finally {
assertEquals(prevCommitCount, roleService.getCommitCount());
assertEquals(prevRollbackCount + 2, roleService.getRollbackCount());
}
}
Transaction 서비스를 직접 얻어온 후에 위와 같이 try~catch 구문 내에서 Transaction 서비스를 이용하여, 적절히 begin, commit, rollback을 수행한다. 이 때, TransactionDefinition와 TransactionStatus 객체를 적절히 이용하면 된다.
Spring Data는 스프링 프레임워크의 하위 프로젝트 중 하나로, 데이터 액세스를 단순화하고 보다 쉽게 관리할 수 있도록 지원하는 도구 모음이다. 주로 데이터베이스와의 상호 작용을 다루며, 다양한 데이터 저장소 및 데이터 액세스 기술을 지원한다.
여기서는 NoSQL 데이터베이스인 R2DBC, Spring Data MongoDB, Cassandra, Redis와 Spring Reactive 연동에 전자정부 표준프레임워크에서 지원하는 라이브러리에 대해 설명한다.
자세한 내용은 아래 페이지에서 확인할 수 있다.
R2DBC(Relational Reactive Database Connectivity)는 Reactive 프로그래밍 모델을 기반으로 하는 비동기적인 방식으로 관계형 데이터베이스와 상호 작용하기 위한 자바 라이브러리로 Spring WebFlux와 함께 사용하여 비동기 논블로킹 방식의 애플리케이션을 구성할 수 있다. 이를 통해 리액티브 애플리케이션 스택에서 관계형 데이터 액세스 기술을 사용하는 Spring 기반 애플리케이션을 더 쉽게 빌드할 수 있다.
R2DBC를 사용하여 데이터베이스에 액세스하기 위해 가장 먼저 해야 할 일은 JDBC의 DataSource와 비슷한 역할을 하는 ConnectionFactory 객체를 만드는 것이다.
ConnectionFactory를 생성하는 가장 간단한 방법은 ConnectionFactories 클래스를 사용하는 것인데 이 클래스에는 ConnectionFactoryOptions 객체를 받아 ConnectionFactory를 반환하는 정적 메서드가 있다.
ConnectionFactory의 인스턴스는 하나만 필요하며, 데이터베이스 종류 및 데이터베이스 드라이버에 따라 구현체가 다를 수 있으므로 공통으로 사용할 수 있게 실행환경에 구성하고 애플리케이션 구성에서 필요할 때마다 주입을 통해 사용할 수 있도록 제공한다.
package org.egovframe.rte.psl.reactive.r2dbc.connect;
public class EgovR2dbcConnectionFactory {
public ConnectionFactory connectionFactory() {
return ConnectionFactories.get(this.r2dbcUrl);
}
}
package egovframework.webflux.config;
import org.egovframe.rte.psl.reactive.r2dbc.connect.EgovR2dbcConnectionFactory;
......
@Configuration
public class EgovR2dbcConfig {
@Bean(name="connectionFactory")
public ConnectionFactory connectionFactory() {
EgovR2dbcConnectionFactory egovR2dbcConnectionFactory = new EgovR2dbcConnectionFactory(this.r2dbcUrl);
return egovR2dbcConnectionFactory.connectionFactory();
}
}
R2DBC는 스프링 생태계와 통합되어 있어 스프링 기반 애플리케이션에서 쉽게 사용할 수 있으며, 스프링 데이터 R2DBC 프로젝트를 통해 스프링의 강력한 기능과 R2DBC의 비동기 데이터베이스 액세스를 결합할 수 있다.
R2dbcEntityTemplate은 객체와 관계형 데이터베이스 간의 매핑을 지원하므로 SQL 쿼리를 직접 작성하지 않고도 객체를 데이터베이스 테이블에 매핑할 수 있으며, CRUD 작업을 단순화하여 개발자는 복잡한 SQL 쿼리를 작성하는 대신 객체 지향적인 방식으로 데이터베이스와 상호 작용할 수 있어 코드 가독성이 향상되고 유지 보수가 용이해진다.
@Repository 클래스에 EgovR2dbcRepository 클래스를 extends 하여 insertData, updateData, deleteData, selectAllData 메소드를 활용한다.
package org.egovframe.rte.psl.reactive.r2dbc.repository;
public class EgovR2dbcRepository<T> extends R2dbcEntityTemplate {
public EgovR2dbcRepository(ConnectionFactory connectionFactory) {
super(connectionFactory);
}
public Flux<T> selectAllData(Query query, Class<T> entityClass) {
return select(query, entityClass);
}
public Mono<T> selectOneData(Query query, Class<T> entityClass) {
return selectOne(query, entityClass);
}
public Mono<Long> countData(Query query, Class<T> entityClass) {
return count(query, entityClass);
}
public Mono<T> insertData(T entity) {
return insert(entity);
}
public Mono<T> updateData(T entity) {
return update(entity);
}
public Mono<T> deleteData(T entity) {
return delete(entity);
}
}
Spring Data MongoDB 프로젝트는 MongoDB 문서 스타일 데이터 저장소를 사용하는 솔루션 개발에 Spring의 핵심 개념을 적용하여 문서를 저장하고 쿼리하기 위한 높은 수준의 추상화 템플릿을 제공한다. Spring 프레임워크에서 제공하는 JDBC 지원과 유사하다는 것을 알 수 있다.
Spring WebFlux에서 MongoDB 데이터베이스와 연결을 설정하고 관리하기 위해서는 ReactiveMongoDatabaseFactory 인터페이스의 구현클래스인 SimpleReactiveMongoDatabaseFactory 클래스를 사용하여, 연결 풀링이나 커넥션 관리 기능 등을 추상화하여 데이터 액세스 작업에 집중할 수 있게 한다.
package org.egovframe.rte.psl.reactive.mongodb.connect;
public class EgovMongoDbConnectionFactory {
public ReactiveMongoDatabaseFactory reactiveMongoDatabaseFactory() {
ConnectionString connectionString = new ConnectionString(this.mongoDbUrl);
MongoClientSettings mongoClientSettings = MongoClientSettings.builder()
.applyConnectionString(connectionString).build();
return new SimpleReactiveMongoDatabaseFactory(
MongoClients.create(mongoClientSettings), this.mongoDbName);
}
}
package egovframework.webflux.config;
import org.egovframe.rte.psl.reactive.mongodb.connect.EgovMongoDbConnectionFactory;
......
@Configuration
public class EgovMongodbConfig {
@Bean(name="reactiveMongoDatabaseFactory")
public ReactiveMongoDatabaseFactory reactiveMongoDatabaseFactory() {
EgovMongoDbConnectionFactory egovMongoDbConnectionFactory = new EgovMongoDbConnectionFactory(this.mongoDBName, this.mongoDBUrl);
return egovMongoDbConnectionFactory.reactiveMongoDatabaseFactory();
}
@Bean
public ReactiveMongoTransactionManager transactionManager(@Qualifier("reactiveMongoDatabaseFactory") ReactiveMongoDatabaseFactory reactiveMongoDatabaseFactory) {
return new ReactiveMongoTransactionManager(reactiveMongoDatabaseFactory);
}
}
ReactiveMongoTemplate 클래스를 이용하여 Repository를 구성하면 MongoDB와 상호 작용하는 동안 Reactive 프로그래밍 모델을 활용할수 있고, Async Non-Blocking 데이터 처리가 가능하여 애플리케이션이 데이터베이스 작업을 대기하지 않고 다른 작업을 수행할 수 있도록 하며, 성능 향상을 실현할 수 있다. 또한 높은 동시 요청을 처리하기 위해 최적화되어 있어 논블로킹 작업을 통해 많은 동시 연결을 처리할 수 있으므로, 대규모 및 고트래픽 애플리케이션에 적합하다. 그리고 Reactive 프로그래밍은 자원을 효율적으로 사용할 수 있도록 도와준다. MongoDB 작업이 블로킹되지 않으므로 스레드 풀 및 메모리 자원을 효율적으로 관리할 수 있으며 MongoDB 연결을 관리하고 에러 처리를 담당하여 연결 풀링, 재시도 메커니즘 및 예외 처리를 제공하여 안정성을 향상시킨다.
@Repository 클래스에 EgovMongoDbRepository 클래스를 extends 하여 insertData, updateData, deleteData, selectAllData 메소드를 활용한다.
package org.egovframe.rte.psl.reactive.mongodb.repository;
public class EgovMongoDbRepository<T> extends ReactiveMongoTemplate {
public EgovMongoDbRepository(ReactiveMongoDatabaseFactory reactiveMongoDatabaseFactory) {
super(reactiveMongoDatabaseFactory);
}
public Flux<T> selectAllData(Query query, Class<T> entityClass) {
return find(query, entityClass);
}
public Mono<T> selectOneData(Query query, Class<T> entityClass) {
return findOne(query, entityClass);
}
public Mono<Long> countData(Query query, Class<T> entityClass) {
return count(query, entityClass);
}
public Mono<T> insertData(T objectToSave) {
return insert(objectToSave);
}
public Mono<T> updateData(Query query, Update update, Class<T> entityClass) {
return findAndModify(query, update, entityClass);
}
public Mono<T> deleteData(Query query, Class<T> entityClass) {
return findAndRemove(query, entityClass);
}
}
Cassandra를 위한 Spring 데이터 프로젝트는 핵심 Spring 개념을 Cassandra 컬럼형 데이터 저장소를 사용하는 솔루션 개발에 적용하여 문서를 저장하고 쿼리하기 위한 높은 수준의 추상화 템플릿을 제공한다. Spring 프레임워크에서 제공하는 JDBC 지원과 유사하다는 것을 알 수 있다.
Spring Data Cassandra와 Spring WebFlux를 함께 사용하여 Cassandra 데이터베이스와의 비동기적인 상호 작용을 지원하기 위해 Spring Data Cassandra에서 제공하는 DefaultBridgedReactiveSession 클래스를 사용한다. 해당 클래스를 사용하여 Cassandra 클러스터에 대한 연결을 설정하고, 세션을 관리하며 비동기 쿼리를 실행하고 결과를 처리할 수 있다.
package org.egovframe.rte.psl.reactive.cassandra.connect;
public class EgovCassandraConfiguration {
public ReactiveSession reactiveSession() {
return new DefaultBridgedReactiveSession(
CqlSession.builder()
.withLocalDatacenter(getDataCenterName())
.withKeyspace(getKeyspaceName())
.addContactPoint(InetSocketAddress.createUnresolved(getContactPoint(), getPort()))
.withAuthCredentials(getUsername(), getPassword())
.build()
);
}
}
package egovframework.webflux.config;
import org.egovframe.rte.psl.reactive.cassandra.connect.EgovCassandraConfiguration;
......
@Configuration
public class EgovCassandraConfig {
@Bean(name="reactiveSession")
public ReactiveSession reactiveSession() {
EgovCassandraConfiguration egovCassandraConfiguration = new EgovCassandraConfiguration();
egovCassandraConfiguration.setDataCenterName(this.dataCenterName);
egovCassandraConfiguration.setKeyspaceName(this.keyspaceName);
egovCassandraConfiguration.setContactPoint(this.contactPoints);
egovCassandraConfiguration.setPort(this.port);
egovCassandraConfiguration.setUsername(this.username);
egovCassandraConfiguration.setPassword(this.password);
return egovCassandraConfiguration.reactiveSession();
}
}
Spring WebFlux와 ReactiveCassandraTemplate을 사용하면 비동기, 논블로킹 처리와 높은 가용성을 통한 효율적인 Cassandra 데이터베이스 상호 작용을 달성할 수 있으며 확장성, 효율성, 그리고 반응형 스트리밍을 통해 데이터베이스 처리를 최적화할 수 있다. 또한 Flux 및 Mono와 같은 리액티브 타입을 사용하여 스트림 데이터를 다룰 수 있으며, 실시간 데이터 처리 및 웹 소켓과 같은 실시간 기능을 구현하기가 용이하다.
@Repository 클래스에 EgovCassandraRepository 클래스를 extends 하여 insertData, updateData, deleteData, selectAllData 메소드를 활용한다.
package org.egovframe.rte.psl.reactive.cassandra.repository;
public class EgovCassandraRepository<T> extends ReactiveCassandraTemplate {
public EgovCassandraRepository(ReactiveSession reactiveSession) {
super(reactiveSession);
}
public Flux<T> selectAllData(Query query, Class<T> entityClass) {
return select(query, entityClass);
}
public Mono<T> selectOneData(Query query, Class<T> entityClass) {
return selectOne(query, entityClass);
}
public Mono<Long> countData(Query query, Class<T> entityClass) {
return count(query, entityClass);
}
public Mono<T> insertData(T entity) {
return insert(entity);
}
public Mono<T> updateData(T entity) {
return update(entity);
}
public Mono<T> deleteData(T entity) {
return delete(entity);
}
}
Spring Data Redis 프로젝트는 키-값 스타일 데이터 저장소를 사용하여 솔루션 개발에 핵심 Spring 개념을 적용하여 메시지를 주고받기 위한 높은 수준의 추상화 템플릿을 제공한다. Spring 프레임워크의 JDBC 지원과 유사하다는 것을 알 수 있다.
Spring Data Redis와 Spring WebFlux를 함께 사용하여 Redis 데이터베이스와의 비동기적인 상호 작용을 지원하기 위해 Spring Data Redis에서 제공하는 ReactiveRedisConnectionFactory 인터페이스의 구현클래스인 LettuceConnectionFactory 클래스를 사용한다. 해당 클래스를 사용하여 데이터베이스 연결을 설정하고, 세션을 관리하며 비동기 쿼리를 실행하고 결과를 처리할 수 있다.
package org.egovframe.rte.psl.reactive.redis.connect;
public class EgovRedisConfiguration {
public ReactiveRedisConnectionFactory reactiveRedisConnectionFactory() {
return new LettuceConnectionFactory(this.host, this.port);
}
}
package egovframework.webflux.config;
@Configuration
public class EgovRedisConfig {
@Bean(name="reactiveRedisConnectionFactory")
public ReactiveRedisConnectionFactory reactiveRedisConnectionFactory() {
EgovRedisConfiguration egovRedisConfiguration = new EgovRedisConfiguration(this.host, this.port);
return egovRedisConfiguration.reactiveRedisConnectionFactory();
}
@Bean(name="idsSerializationContext")
public RedisSerializationContext<String, Ids> idsReactiveRedisTemplate() {
Jackson2JsonRedisSerializer<Ids> serializer = new Jackson2JsonRedisSerializer<>(Ids.class);
RedisSerializationContext.RedisSerializationContextBuilder<String, Ids> builder =
RedisSerializationContext.newSerializationContext(ew StringRedisSerializer());
return builder.value(serializer).hashValue(serializer).hashKey(serializer).build();
}
@Bean(name="sampleSerializationContext")
public RedisSerializationContext<String, Sample> sampleReactiveRedisTemplate() {
Jackson2JsonRedisSerializer<Sample> serializer = new Jackson2JsonRedisSerializer<>(Sample.class);
RedisSerializationContext.RedisSerializationContextBuilder<String, Sample> builder =
RedisSerializationContext.newSerializationContext(new StringRedisSerializer());
return builder.value(serializer).hashValue(serializer).hashKey(serializer).build();
}
}
Spring WebFlux와 ReactiveRedisTemplate을 이용을 사용하면 Async Non-Blocking 처리로 I/O 밀집적인 작업을 효율적으로 처리 가능하여 대량의 동시 요청을 효과적으로 처리하고 확장 가능한 애플리케이션을 개발할 수 있다. 또한, Redis 데이터베이스에 비동기 쿼리를 보내고 전반적인 성능을 향상시킬 수 있어 데이터 스트리밍과 실시간 데이터 처리에 적합하다. 그리고, Spring 프레임워크와의 원활한 통합으로 풍부한 개발 환경을 제공하며 모델의 일관성을 유지하고 유지 관리를 간편하게 할 수 있다.
@Repository 클래스에 EgovRedisRepository 클래스를 extends 하여 insertData, updateData, deleteData, selectData 메소드를 활용한다.
package org.egovframe.rte.psl.reactive.redis.repository;
public class EgovRedisRepository<T> extends ReactiveRedisTemplate {
public EgovRedisRepository(ReactiveRedisConnectionFactory connectionFactory, RedisSerializationContext serializationContext) {
super(connectionFactory, serializationContext);
}
public Flux<T> selectData(String redisKey) {
return opsForList().range(redisKey, 0, -1);
}
public Mono<Long> findIndex(String redisKey, T entity) {
return opsForList().indexOf(redisKey, entity);
}
public Mono<Long> countData(String redisKey) {
return opsForList().size(redisKey);
}
public Mono<T> insertData(String redisKey, T entity) {
return opsForList().leftPush(redisKey, entity);
}
public Mono<T> updateData(String redisKey, long idx, T entity) {
return opsForList().set(redisKey, idx, entity);
}
public Mono<Boolean> deleteAllData(String redisKey) {
return opsForList().delete(redisKey);
}
public Mono<Boolean> deleteData(String redisKey, T entity) {
return opsForList().remove(redisKey, 0, entity);
}
}
연계통합 레이어는 타 시스템과의 연동기능을 지원한다.
Naming 서비스는 Java Naming and Directory Interface(JNDI) API를 이용하여 자원(Resource)를 찾을 수 있도록 도와주는 서비스이다. Naming 서비스를 지원하는 Naming 서버에 자원을 등록하여 다른 어플리케이션에서 사용할 수 있도록 공개하고, Naming 서버에 등록되어 있는 자원을 찾아와서 이용할 수 있게 한다.

Java Naming and Directory Interface(JNDI)는 Java 소프트웨어 클라이언트가 이름(name)을 이용하여 데이터 및 객체를 찾을 수 있도록 도와주는 디렉토리 서비스에 대한 Java API이다.
Naming 서비스는 사용하는 방식에는 Spring XML Configuration 파일에 설정하는 방식과 JNDI API를 wrapping한 JndiTemplate class를 사용하는 방식이 있다.
Spring XML Configuration 파일에 설정하는 방식 :
Spring XML Configuration 설정파일에 JNDI 객체를 bean으로 등록하는 방식으로, JNDI 객체를 Lookup만 할 수 있다. 일반적으로 가장 많이 사용된다.
JNDI API를 wrapping한 JndiTemplate class를 사용하는 방식 :
Spring Framework에서 JNDI API를 쉽게 사용할 수 있도록 제공하는 JndiTemplate class를 직접 사용하는 방식으로, JNDI API 기능을 모두 사용해야 할 경우 사용하는 방식이다.
Spring Framework는 XML Configuration 파일에 JNDI 객체를 설정할 수 있다. 단, 설정 파일을 통해서는 JNDI 객체를 lookup하는 것만 가능하므로, bind, rebind, unbind 기능을 사용하려면 Using JndiTemplate 방식을 사용해야 한다.
Spring Framework은 XML Configuration을 간편하게 할 수 있게 하기 위해 2.0 버전부터 jee tag를 제공하고 있다. 전자정부 개발프레임워크는 Spring 2.5 이상을 기반으로 하기 때문에 본 가이드는 jee tag를 사용한 방식만을 설명한다.
jee tag를 사용하기 위해서는 Spring XML Configuration 파일의 머릿말에 namespace와 schemaLocation를 추가해야 한다.
namespace : xmlns:jee=“http://www.springframework.org/schema/jee"
schemaLocation : http://www.springframework.org/schema/jee http://www.springframework.org/schema/jee/spring-jee.xsd
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:jee="http://www.springframework.org/schema/jee"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/jee http://www.springframework.org/schema/jee/spring-jee.xsd">
<!-- <bean/> definitions here -->
</beans>
jndi-lookup tag는 JNDI 객체를 찾아서 bean으로 등록해주는 tag이다.
<jee:jndi-lookup id="bean id"
jndi-name="jndi name"
cache="true or false"
resource-ref="true or false"
lookup-on-startup="true or false"
expected-type="java class"
proxy-interface="java class">
<jee:environment>
name=value
ping=pong
...
</jee:environment>
</jee:jndi-lookup>
jndi-lookup tag는 Spring Framework의 JndiObjectFactoryBean class와 1:1로 매핑된다. tag의 attribute 값은 다음과 같다.
| id | Spring XML Configuration의 bean id이다. | N | String | ||
| jndi-name | 찾고자 하는 JNDI 객체의 이름이다. | N | String | ||
| cache | 한번 찾은 JNDI 객체에 대한 cache여부를 나타낸다. | Y | boolean | true | |
| resource-ref | J2EE Container 내에서 찾을지 여부를 나타낸다. | Y | boolean | false | |
| lookup-on-startup | 시작시에 lookup을 수행할지 여부를 타나낸다. | Y | boolean | true | |
| expected-type | 찾는 JNDI 객체를 assign할 타입을 나타낸다. | Y | Class | 값이 지정되지 않았을 경우 무시한다. | |
| proxy-interface | JNDI 객체를 사용하기 위한 Proxy Interface이다. | Y | Class | 값이 지정되지 않았을 경우 무시한다. |
jndi-lookup tag의 element인 environment tag는 JNDI Environment 변수값을 등록할 때 사용한다. environment tag는 ‘foo=bar’ 와 같이 <변수명>=<변수값> 형태의 List를 값으로 가진다.
Simple
가장 단순한 설정으로 이름만을 사용하여 JNDI 객체를 찾아준다. 아래 이름 “jdbc/MyDataSource”로 등록되어 있는 JNDI 객체를 찾아 “userDao” Bean의 “dataSource” property로 Dependency Injection하는 예제이다.
<jee:jndi-lookup id="dataSource" jndi-name="jdbc/MyDataSource"/>
<bean id="userDao" class="com.foo.JdbcUserDao">
<!-- Spring will do the cast automatically (as usual) -->
<property name="dataSource" ref="dataSource"/>
</bean>
With single JNDI environment settings
아래는 단일 JNDI 환경 설정을 사용하여 JNDI 객체를 찾아오는 예제이다.
<jee:jndi-lookup id="dataSource" jndi-name="jdbc/MyDataSource">
<jee:environment>foo=bar</jee:environment>
</jee:jndi-lookup>
With multiple JNDI environment settings
아래는 복수 JNDI 환경 설정을 사용하여 JNDI 객체를 찾아오는 예제이다.
<jee:jndi-lookup id="dataSource" jndi-name="jdbc/MyDataSource">
<!-- newline-separated, key-value pairs for the environment (standard Properties format) -->
<jee:environment>
foo=bar
ping=pong
</jee:environment>
</jee:jndi-lookup>
Complex
아래는 이름 외 다양한 설정을 통해 JNDI 객체를 찾아오는 예제이다.
<jee:jndi-lookup id="dataSource"
jndi-name="jdbc/MyDataSource"
cache="true"
resource-ref="true"
lookup-on-startup="false"
expected-type="com.myapp.DefaultFoo"
proxy-interface="com.myapp.Foo"/>
local-slsb tag는 EJB Stateless SessionBean을 참조하기 위한 tag이다.
<jee:local-slsb id="bean id"
jndi-name="JNDI name"
business-interface="Java Class"
cache-home="true or false"
lookup-home-on-startup="true or false"
resource-ref="true or false">
<jee:environment>
name=value
ping=pong
...
</jee:environment>
</jee:local-slsb>
locak-slsb tag는 Spring Framework의 LocalStatelessSessionProxyFactoryBean class와 1:1로 매핑된다. tag의 attribute는 다음과 같다.
| id | Spring XML Configuration의 bean id이다. | N | String | ||
| jndi-name | 찾고자 하는 EJB의 JNDI 이름이다. | N | String | ||
| business-interface | Proxing할 EJB의 Business interface이다. | N | Class | ||
| cache-home | 한번 찾은 EJB Home 객체에 대한 cache여부를 나타낸다. | Y | boolean | true | |
| lookup-home-on-startup | 시작 시에 lookup을 수행할지 여부를 나타낸다. | Y | boolean | true | |
| resource-ref | J2EE Container 내에서 찾을지 여부를 나타낸다. | Y | boolean | false |
local-slsb tag의 element인 environment tag는 JNDI Environment 변수값을 등록할 때 사용한다. evironment tag는 ‘foo=bar’ 와 같이 <변수명>=<변수값> 형태의 List를 값으로 가진다.
Simple
간단히 사용하는 예제이다.
<jee:local-slsb id="simpleSlsb" jndi-name="ejb/RentalServiceBean"
business-interface="com.foo.service.RentalService"/>
Complex
local-slsb tag를 사용하기 위해 다양한 설정 값을 이용하는 예제이다.
<jee:local-slsb id="complexLocalEjb"
jndi-name="ejb/RentalServiceBean"
business-interface="com.foo.service.RentalService"
cache-home="true"
lookup-home-on-startup="true"
resource-ref="true"/>
remote-slsb tag는 remote EJB Stateless SessionBean을 참조하기 위한 tag이다.
<jee:remote-slsb id="bean id"
jndi-name="JNDI name"
business-interface="Java Class"
cache-home="true or false"
lookup-home-on-startup="true or false"
resource-ref="true or false"
home-interface="Java Class"
refresh-home-on-connect-failure="true or false">
<jee:environment>
name=value
ping=pong
...
</jee:environment>
</jee:remote-slsb>
remote-slsb tag는 Spring Framework의 SimpleRemoteStatelessSessionProxyFactoryBean class와 1:1로 매핑된다. tag의 attribute는 아래와 같다.
| id | Spring XML Configuration의 bean id이다. | N | String | ||
| jndi-name | 찾고자 하는 EJB의 JNDI 이름이다. | N | String | ||
| business-interface | Proxing할 EJB의 Business interface이다. | N | Class | ||
| cache-home | 한번 찾은 EJB Home 객체에 대한 cache여부를 나타낸다. | Y | boolean | true | |
| lookup-home-on-startup | 시작 시에 lookup을 수행할지 여부를 나타낸다. | Y | boolean | true | |
| resource-ref | J2EE Container 내에서 찾을지 여부를 나타낸다. | Y | boolean | false | |
| home-interface | EJB Home interface이다. | Y | Class | ||
| refresh-home-on-connect-failure | 연결 실패 시, EJB Home을 reflesh할지 여부를 나타낸다. | Y | boolean | false |
remote-slsb tag의 element인 environment tag는 JNDI Environment 변수값을 등록할 때 사용한다. evironment tag는 ‘foo=bar’ 와 같이 <변수명>=<변수값> 형태의 List를 값으로 가진다.
Simple
간단히 사용하는 예제이다.
<jee:remote-slsb id="complexRemoteEjb"
jndi-name="ejb/MyRemoteBean"
business-interface="com.foo.service.RentalService"
home-interface="com.foo.service.RentalService"/>
Complex
remove-slsb tag를 사용하기 위해 다양한 설정 값을 이용하는 예제이다.
<jee:remote-slsb id="complexRemoteEjb"
jndi-name="ejb/MyRemoteBean"
business-interface="com.foo.service.RentalService"
cache-home="true"
lookup-home-on-startup="true"
resource-ref="true"
home-interface="com.foo.service.RentalService"
refresh-home-on-connect-failure="true"/>
JndiTemplate class는 JNDI API를 쉽게 사용할 수 있도록 제공하는 wrapper class이다.
아래 JndiTemplateSample class의 bind 메소드는 JndiTemplate을 이용하여 argument ‘resource’를 argument ’name’으로 JNDI 객체로 bind한다.
import javax.naming.NamingException;
import org.springframework.jndi.JndiTemplate;
...
public class JndiTemplateSample
{
private JndiTemplate jndiTemplate = new JndiTemplate();
...
public boolean bind(final String name, Object resource)
{
try
{
jndiTemplate.bind(name, resource);
return true;
}
catch (NamingException e)
{
e.printStackTrace();
return false;
}
}
...
}
JndiTemplate을 이용하여 argument ’name’으로 등록되어 있는 자원(resource)를 찾을 수 있다.
public Object lookupResource(final String name)
{
try
{
return jndiTemplate.lookup(name);
}
catch (NamingException e)
{
e.printStackTrace();
return null;
}
}
JndiTemplate의 lookup 메소드는 찾고자 하는 자원의 이름 뿐 아니라 원하는 타입(Type)을 지정할 수 있다.
public Foo lookupFoo(final String fooName)
{
try
{
return jndiTemplate.lookup(fooName, Foo.class);
}
catch (NamingException e)
{
e.printStackTrace();
return null;
}
}
JndiTemplate의 rebind 메소드를 사용하여 자원을 재등록할 수 있다.
public boolean rebind(final String name, Object resource)
{
try
{
jndiTemplate.rebind(name, resource);
return true;
}
catch (NamingException e)
{
e.printStackTrace();
return false;
}
}
JndiTemplate의 unbind 메소드를 사용하여 등록된 자원을 등록해제할 수 있다.
public boolean unbind(final String name)
{
try
{
jndiTemplate.unbind(name);
return true;
}
catch (NamingException e)
{
e.printStackTrace();
return false;
}
}
Integration 서비스는 전자정부 개발프레임워크 기반의 시스템이 타 시스템과의 연계를 위해 사용하는 Interface의 표준을 정의한 것이다.
기존의 전자정부 시스템은 타 시스템과의 연계를 위해 연계 솔루션을 사용하거나 자체 개발한 연계 모듈을 사용해왔다. 기존에 사용된 연계 솔루션 및 자체 연계 모듈은 각각 고유한 설정 및 사용 방식을 가지고 있어, 동일한 연계 서비스라 할지라도 사용하는 연계 모듈에 따라 각기 다른 방식으로 개발되어 왔다. 본 Integration 서비스는 이러한 중복 개발을 없애고, 표준화된 설정 및 사용 방식을 정의하여 개발 효율성을 제고한다.
Integration 서비스를 사용하여 구현된 전자정부 시스템의 아키텍처는 다음과 같다.

Integration 서비스는 연계 서비스 요청 Interface, 연계 서비스 제공 Interface, 연계 메시지 및 메시지 헤더 등을 정의하고 있으며, 연계 서비스 요청 모듈 및 제공 모듈은 연계 Adaptor나 연계 솔루션과 관계 없이 Integration 서비스가 제공하는 Interface와 Class만을 사용하여 연계 업무를 수행할 수 있다. Integration 서비스는 연계 Interface 외에 연계에 필요한 정보를 담기 위한 Metadata를 정의하고 있다. Metadata는 연계 Interface를 사용하기 위해 필요한 최소한의 정보(연계 기관 정보, 연계 시스템 정보, 연계 서비스 정보, 메시지 형식 등)을 정의하고 있다.
Integration 서비스는 연계에 필요한 정보를 정의한 Metadata와 연계 서비스를 사용 및 제공하기 위한 API로 구성된다.
Integration 서비스 Metadata는 연계에 필요한 정보를 정의하고 있다. 본 장은 실제 Integration 서비스로 구현된 연계 Adaptor를 사용하는 방식에 직접적인 도움을 주지는 않는다. 실제 사용법은 연계 서비스 API에서 설명하고 있다. 단, 연계 서비스 API의 핵심 Interface인 EgovIntegrationService의 단위에 해당하는 연계등록정보와, 이와 관련된 기관, 시스템, 서비스 등의 Metadata를 이해하는 것은 API 사용에 도움이 될 수 있다.
Integration 서비스 Metadata의 논리모델은 연계를 위해 필요한 논리적인 정보를 정의한다.
Integration 서비스 Metadata의 논리ERD 및 Entity 설명은 다음과 같다.
"<name> : <data type> <domain>" 이다.
| 기관 | 연계 서비스를 제공 또는 사용하는 기관을 나타낸다. 하나의 기관은 다수의 시스템을 가지고 있다. |
| 시스템 | 연계 서비스를 제공 또는 사용하는 시스템을 나타낸다. 하나의 시스템은 반드시 하나의 기관에 속하며, 다수의 서비스를 가지고 있다. |
| 서비스 | 연계 서비스를 제공하는 단위를 나타낸다. 하나의 서비스는 반드시 하나의 시스템에 속한다. |
| 연계등록정보 | 연계 서비스를 사용하기 위한 단위를 나타낸다. 연계 요청 시스템이 연계 제공 서비스를 사용하기 위해서 등록해야 하는 정보를 담고 있다. |
| 레코드타입 | 연계에 사용되는 메시지의 형태를 나타낸다. <Key, Value> 쌍의 정보를 담고 있는 레코드 형태의 타입을 정의하고 있다. 하나의 레코드타입은 다수의 레코드필드를 가지고 있다. |
| 레코드필드 | 레코드타입에 속하는 내부 필드의 정의를 나타낸다. 필드의 이름과 타입을 정의한다. 하나의 레코드필드는 반드시 하나의 레코드타입에 속한다. |
| 기관ID | CHAR(8) | 연계 서비스를 제공 또는 사용하는 기관의 ID를 나타낸다. | ID 체계 참조 |
| 시스템ID | CHAR(8) | 연계 서비스를 제공 또는 사용하는 시스템의 ID를 나타낸다. | ID 체계 참조 |
| 서비스ID | CHAR(8) | 연계 서비스의 ID를 나타낸다. | ID 체계 참조 |
| 타입ID | VARCHAR(40) | 메시지를 구성하는 각 구성 요소의 형식인 타입의 ID를 나타낸다. | ID 체계 참조 |
| 모듈ID | VARCHAR(40) | 연계 서비스를 제공하기 위해 등록되어 있는 서비스 제공 모듈의 ID를 나타낸다. | ID 체계 참조 |
| Boolean | CHAR(1) | 참/거짓(true/false) 등의 논리값을 나타낸다. | ‘Y’ : true ‘N’ : false |
| Number | INTEGER | 일반적인 정수를 나타낸다. | |
| 일시 | DATETIME | 일자와 시각을 포함하는 일시를 나타낸다. | |
| 이름 | VARCHAR(40) | 기관, 시스템, 서비스 등의 각종 이름을 나타낸다. |
| Entity 명 | 기관 | ||||
| 설명 | 연계 서비스를 제공 또는 사용하는 기관을 나타낸다. 하나의 기관은 다수의 시스템을 가지고 있다. | ||||
| Attribute | |||||
| Seq | PK | Attribute명 | Domain | Data Type | 설명 |
| 1 | Y | 기관ID | 기관ID | CHAR(8) | 기관의 ID이다. |
| 2 | 기관명 | 이름 | VARCHAR(40) | 기관의 이름이다. | |
| Entity 명 | 시스템 | ||||
| 설명 | 연계 서비스를 제공 또는 사용하는 시스템을 나타낸다. 하나의 시스템은 반드시 하나의 기관에 속하며, 다수의 서비스를 가지고 있다. | ||||
| Attribute | |||||
| Seq | PK | Attribute명 | Domain | Data Type | 설명 |
| 1 | Y | 기관ID | 기관ID | CHAR(8) | 기관의 ID이다. |
| 2 | Y | 시스템ID | 시스템ID | CHAR(8) | 시스템의 ID이다. |
| 3 | 시스템명 | 이름 | VARCHAR(40) | 시스템의 이름이다. | |
| 4 | 표준여부 | Boolean | CHAR(1) | 표준 준수 여부를 나타낸다. | |
| Entity 명 | 서비스 | ||||
| 설명 | 연계 서비스를 제공하는 단위를 나타낸다. 하나의 서비스는 반드시 하나의 시스템에 속한다. | ||||
| Attribute | |||||
| Seq | PK | Attribute명 | Domain | Data Type | 설명 |
| 1 | Y | 기관ID | 기관ID | CHAR(8) | 기관의 ID이다. |
| 2 | Y | 시스템ID | 시스템ID | CHAR(8) | 시스템의 ID이다. |
| 3 | Y | 서비스ID | 서비스ID | CHAR(8) | 서비스의 ID이다. |
| 4 | 서비스명 | 이름 | VARCHAR(40) | 서비스의 이름이다. | |
| 5 | 요청메시지타입ID | 타입ID | VARCHAR(40) | 요청 메시지의 타입 ID이다. | |
| 6 | 응답메시지타입ID | 타입ID | VARCHAR(40) | 응답 메시지의 타입 ID이다. | |
| 7 | 응답모듈ID | 모듈ID | VARCHAR(40) | 실제 서비스를 제공하는 응답 모듈의 ID이다. | |
| 8 | 표준여부 | Boolean | CHAR(1) | 표준 준수 여부를 나타낸다. | |
| 9 | 사용여부 | Boolean | CHAR(1) | 서비스의 사용 여부를 나타낸다. | |
| Entity 명 | 연계등록정보 | ||||
| 설명 | 연계 서비스를 사용하기 위한 단위를 나타낸다. 연계 요청 시스템이 연계 제공 서비스를 사용하기 위해서 등록해야 하는 정보를 담고 있다. | ||||
| Attribute | |||||
| Seq | PK | Attribute명 | Domain | Data Type | 설명 |
| 1 | Y | 제공기관ID | 기관ID | CHAR(8) | 서비스를 제공하는 기관의 ID이다. |
| 2 | Y | 제공시스템ID | 시스템ID | CHAR(8) | 서비스를 제공하는 시스템의 ID이다. |
| 3 | Y | 제공서비스ID | 서비스ID | CHAR(8) | 서비스를 제공하는 서비스의 ID이다. |
| 4 | Y | 요청기관ID | 기관ID | CHAR(8) | 서비스를 요청하는 기관의 ID이다. |
| 5 | Y | 요청시스템ID | 시스템ID | CHAR(8) | 서비스를 요청하는 시스템의 ID이다. |
| 6 | DefaultTimeout | Number | INTEGER | 서비스 요청 시 사용되는 default timeout 값이다. | |
| 7 | 사용여부 | Boolean | CHAR(1) | 등록된 연계의 사용 여부를 나타낸다. | |
| 8 | 유효시작일시 | 일시 | DATEIME | 등록된 연계가 유효한 기간의 시작 일시를 나타낸다. | |
| 9 | 유효종료일시 | 일시 | DATEIME | 등록된 연계가 유효한 기간의 종료 일시를 나타낸다. | |
| Entity 명 | 레코드타입 | ||||
| 설명 | 연계에 사용되는 메시지의 형태를 나타낸다. <Key, Value> 쌍의 정보를 담고 있는 레코드 형태의 타입을 정의하고 있다. 하나의 레코드타입은 다수의 레코드필드를 가지고 있다. | ||||
| Attribute | |||||
| Seq | PK | Attribute명 | Domain | Data Type | 설명 |
| 1 | Y | 레코드타입ID | 타입ID | VARCHAR(40) | 레코드 타입의 ID이다. |
| 2 | 레코드타입명 | 이름 | VARCHAR(40) | 레코드 타입의 이름이다. | |
| 3 | 부모레코드타입ID | 타입ID | VARCHAR(40) | 부모 레코드 타입의 ID이다. | |
| Entity 명 | 레코드필드 | ||||
| 설명 | 레코드타입에 속하는 내부 필드의 정의를 나타낸다. 필드의 이름과 타입을 정의한다. 하나의 레코드필드는 반드시 하나의 레코드타입에 속한다. | ||||
| Attribute | |||||
| Seq | PK | Attribute명 | Domain | Data Type | 설명 |
| 1 | Y | 레코드타입ID | 타입ID | VARCHAR(40) | 필드가 속한 레코드의 타입 ID이다. |
| 2 | Y | 필드명 | 이름 | VARCHAR(40) | 필드의 이름이다. |
| 3 | 필드타입ID | 타입ID | VARCHAR(40) | 필드의 타입 ID이다. | |
Integration 서비스 Metadata의 물리모델은 논리모델을 실제 물리적인 DB로 구현하기 위한 모델로서, 아래 물리ERD는 Oracle DB를 가정하여 작성된 것이다. 물리모델은 Hibernate 등과 같은 Object Relational Mapping(ORM)을 사용하여 Access하는 것을 고려하여, 복수의 Attribute를 Identifier로 갖는 Entity를 Table로 변환할 때 Surrogate Key를 도입하고, 기존 Identifier는 Unique Constraints로 적용하여 정의되었다.
Integration 서비스 Metadata의 물리ERD 및 Table 설명은 다음과 같다.
ERD의 Table Column의 Notation은 "<name> : <data type> <domain> <null option> <key>*" 이다.
물리ERD의 경우, 각 연계 Adaptor 또는 연계 솔루션, 또는 시스템에 따라 사용하는 DB가 달라지므로 Data Type 등이 변경될 수 있다.

| ORGANIZATION | 기관 | 연계 서비스를 제공 또는 사용하는 기관을 나타낸다. 하나의 기관은 다수의 시스템을 가지고 있다. |
| SYSTEM | 시스템 | 연계 서비스를 제공 또는 사용하는 기관을 나타낸다. 하나의 기관은 다수의 시스템을 가지고 있다. |
| SERVICE | 서비스 | 연계 서비스를 제공하는 단위를 나타낸다. 하나의 서비스는 반드시 하나의 시스템에 속한다. |
| INTEGRATION | 연계등록정보 | 연계 서비스를 사용하기 위한 단위를 나타낸다. 연계 요청 시스템이 연계 제공 서비스를 사용하기 위해서 등록해야 하는 정보를 담고 있다. |
| RECORD_TYPE | 레코드타입 | 연계에 사용되는 메시지의 형태를 나타낸다. <Key, Value> 쌍의 정보를 담고 있는 레코드 형태의 타입을 정의하고 있다. 하나의 레코드타입은 다수의 레코드필드를 가지고 있다. |
| RECORD_TYPE_FIELD | 레코드필드 | 레코드타입에 속하는 내부 필드의 정의를 나타낸다. 필드의 이름과 타입을 정의한다. 하나의 레코드필드는 반드시 하나의 레코드타입에 속한다. |
| OrganizationId | CHAR(8) | 연계 서비스를 제공 또는 사용하는 기관의 ID를 나타낸다. | ID 체계 참조 |
| SystemId | CHAR(8) | 연계 서비스를 제공 또는 사용하는 시스템의 ID를 나타낸다. | ID 체계 참조 |
| ServiceId | CHAR(8) | 연계 서비스의 ID를 나타낸다. | ID 체계 참조 |
| TypeId | VARCHAR2(40) | 메시지를 구성하는 각 구성 요소의 형식인 타입의 ID를 나타낸다. | ID 체계 참조 |
| BeanId | VARCHAR2(40) | 연계 서비스를 제공하기 위해 등록되어 있는 서비스 제공 모듈의 ID를 나타낸다. | ID 체계 참조 |
| Boolean | CHAR(1) | 참/거짓(true/false) 등의 논리값을 나타낸다. | ‘Y’ : true ‘N’ : false |
| Number | INTEGER | 일반적인 정수를 나타낸다. | |
| Datetime | DATEIME | 일자와 시각을 포함하는 일시를 나타낸다. | |
| Name | VARCHAR2(40) | 기관, 시스템, 서비스 등의 각종 이름을 나타낸다. | |
| SurrogateKey | VARCHAR2(20) | Composite 형태의 Primary Key를 대체하기 위한 키 |
| Table 명 | ORGANIZATION | Entity | 기관 | ||||
| 설명 | 연계 서비스를 제공 또는 사용하는 기관을 나타낸다. 하나의 기관은 다수의 시스템을 가지고 있다. | ||||||
| Column | |||||||
| Seq | PK | Column명 | 한글명 | Domain | Data Type | Null | 설명 |
| 1 | Y | ORGANIZATION_ID | 기관ID | OrganizationId | CHAR(8) | N | 기관의 ID이다. |
| 2 | ORGANIZATION_NAME | 기관명 | Name | VARCHAR2(40) | N | 기관의 이름이다. | |
| Constraints | |||||||
| PRIMARY KEY (ORGANIZATION_ID) | |||||||
| Table 명 | SYSTEM | Entity | 시스템 | ||||
| 설명 | 연계 서비스를 제공 또는 사용하는 시스템을 나타낸다. 하나의 시스템은 반드시 하나의 기관에 속하며, 다수의 서비스를 가지고 있다. | ||||||
| Column | |||||||
| Seq | PK | Column명 | 한글명 | Domain | Data Type | Null | 설명 |
| 1 | Y | SYSTEM_KEY | 시스템KEY | SurrogateKey | VARCHAR2(20) | N | 시스템의 Surrogate Key이다. |
| 2 | ORGANIZATION_ID | 기관ID | OrganizationId | CHAR(8) | N | 기관의 ID이다. | |
| 3 | SYSTEM_ID | 시스템ID | SystemId | CHAR(8) | N | 시스템의 ID이다. | |
| 4 | SYSTEM_NAME | 시스템명 | Name | VARCHAR2(40) | N | 시스템의 이름이다. | |
| 5 | STANDARD_YN | 표준여부 | Boolean | CHAR(1) | N | 표준 준수 여부를 나타낸다. | |
| Constraints | |||||||
| PRIMAEY KEY (SYSTEM_KEY) | |||||||
| UNIQUE (ORGANIZATION_ID, SYSTEM_ID) | |||||||
| FOREIGN KEY (ORGANIZATION_ID) REFERENCES ORGANIZATION (ORGANIZATION_ID) | |||||||
| Table 명 | SERVICE | Entity | 서비스 | ||||
| 설명 | 연계 서비스를 제공하는 단위를 나타낸다. 하나의 서비스는 반드시 하나의 시스템에 속한다. | ||||||
| Column | |||||||
| Seq | PK | Column명 | 한글명 | Domain | Data Type | Null | 설명 |
| 1 | Y | SERVICE_KEY | 서비스KEY | SurrogateKey | VARCHAR2(20) | N | 서비스의 Surrogate Key이다. |
| 2 | SYSTEM_KEY | 시스템KEY | SurrogateKey | VARCHAR2(20) | N | 시스템의 Surrogate Key이다. | |
| 3 | SERVICE_ID | 서비스ID | ServiceId | CHAR(8) | N | 서비스의 ID이다. | |
| 4 | SERVICE_NAME | 서비스명 | Name | VARCHAR2(40) | N | 서비스의 이름이다. | |
| 5 | REQUEST_MESSAGE_TYPE_ID | 요청메시지타입ID | TypeId | VARCHAR2(40) | N | 요청 메시지의 타입 ID이다. | |
| 6 | RESPONSE_MESSAGE_TYPE_ID | 응답메시지타입ID | TypeId | VARCHAR2(40) | N | 응답 메시지의 타입 ID이다. | |
| 7 | SERVICE_PROVIDER_BEAN_ID | 응답모듈ID | BeanId | VARCHAR2(40) | Y | 실제 서비스를 제공하는 응답 모듈의 ID이다. | |
| 8 | STANDARD_YN | 표준여부 | Boolean | CHAR(1) | N | 표준 준수 여부를 나타낸다. | |
| 9 | USING_YN | 사용여부 | Boolean | CHAR(1) | N | 서비스의 사용 여부를 나타낸다. | |
| Constraints | |||||||
| PRIMARY KEY (SERVICE_KEY) | |||||||
| UNIQUE (SYSTEM_KEY, SERVICE_ID) | |||||||
| FOREIGN KEY (SYSTEM_KEY) REFERENCES SYSTEM (SYSTEM_KEY) | |||||||
| Table 명 | INTEGRATION | Entity | 연계등록정보 | ||||
| 설명 | 연계 서비스를 사용하기 위한 단위를 나타낸다. 연계 요청 시스템이 연계 제공 서비스를 사용하기 위해서 등록해야 하는 정보를 담고 있다. | ||||||
| Column | |||||||
| Seq | PK | Column명 | 한글명 | Domain | Data Type | Null | 설명 |
| 1 | Y | INTEGRATION_ID | 연계ID | Surrogate Key | VARCHAR2(20) | N | 연계등록정보의 Surrogate Key이다. |
| 2 | PROVIDER_SERVICE_KEY | 제공서비스KEY | Surrogate Key | VARCHAR2(20) | N | 서비스를 제공하는 서비스의 Surrogate Key이다. | |
| 3 | CONSUMER_SYSTEM_KEY | 요청시스템KEY | Surrogate Key | VARCHAR2(20) | N | 서비스를 요청하는 시스템의 Surrogate Key이다. | |
| 4 | DEFAULT_TIMEOUT | DefaultTimeout | Number | INTEGER | N | 서비스 요청 시 사용되는 default timeout 값이다. | |
| 5 | USING_YN | 사용여부 | Boolean | CHAR(1) | N | 등록된 연계의 사용 여부를 나타낸다. | |
| 6 | VALIDATE_FROM | 유효시작일시 | Datetime | DATETIME | Y | 등록된 연계가 유효한 기간의 시작 일시를 나타낸다. | |
| 7 | VALIDATE_TO | 유효종료일시 | Datetime | DATETIME | Y | 등록된 연계가 유효한 기간의 종료 일시를 나타낸다. | |
| Constraints | |||||||
| PRIMARY KEY (INTEGRATION_ID) | |||||||
| UNIQUE (PROVIDER_SERVICE_KEY, CONSUMER_SYSTEM_KEY) | |||||||
| FOREIGN KEY (PROVIDER_SERVICE_KEY) REFERENCES SERVICE (SERVICE_KEY) | |||||||
| ROREIGN KEY (CONSUMER_SYSTEM_KEY) REFERENCES SYSTEM (SYSTEM_KEY) | |||||||
| Table 명 | RECORD_TYPE | Entity | 레코드타입 | ||||
| 설명 | 연계에 사용되는 메시지의 형태를 나타낸다. <Key, Value> 쌍의 정보를 담고 있는 레코드 형태의 타입을 정의하고 있다. 하나의 레코드타입은 다수의 레코드필드를 가지고 있다. | ||||||
| Column | |||||||
| Seq | PK | Column명 | 한글명 | Domain | Data Type | Null | 설명 |
| 1 | Y | RECORD_TYPE_ID | 레코드타입ID | TypeId | VARCHAR2(40) | N | 레코드 타입의 ID이다. |
| 2 | RECORD_TYPE_NAME | 레코드타입명 | Name | VARCHAR2(40) | N | 레코드 타입의 이름이다. | |
| 3 | PARENT_RECORD_TYPE_ID | 부모레코드타입ID | TypeId | VARCHAR2(40) | Y | 부모 레코드 타입의 ID이다. | |
| Constraints | |||||||
| PRIMARY KEY (RECORD_TYPE_ID) | |||||||
| FOREIGN KEY (PARENT_RECORD_TYPE_ID) REFERENCES RECORD_TYPE (RECORD_TYPE_ID) | |||||||
| Table 명 | RECORD_TYPE_FIELD | Entity | 레코드필드 | ||||
| 설명 | 레코드타입에 속하는 내부 필드의 정의를 나타낸다. 필드의 이름과 타입을 정의한다. 하나의 레코드필드는 반드시 하나의 레코드타입에 속한다. | ||||||
| Column | |||||||
| Seq | PK | Column명 | 한글명 | Domain | Data Type | Null | 설명 |
| 1 | Y | RECORD_TYPE_ID | 레코드타입ID | TypeId | VARCHAR2(40) | N | 필드가 속한 레코드의 타입 ID이다. |
| 2 | Y | RECORD_FIELD_NAME | 필드명 | Name | VARCHAR2(40) | N | 필드의 이름이다. |
| 3 | RECORD_FIELD_TYPE_ID | 필드타입ID | TypeId | VARCHAR2(40) | N | 필드의 타입 ID이다. | |
| Constraints | |||||||
| PRIMARY KEY (RECORD_TYPE_ID, RECORD_FIELD_NAME) | |||||||
| FOREIGN KEY (RECORD_TYPE_ID) REFERENCES RECORD_TYPE (RECORD_TYPE_ID) | |||||||
Integration 서비스의 Metadata를 읽어오기 위한 Java Class를 정의하고 있다.

Integration 서비스 Metadata의 ID 체계는 다음과 같다.
| 기관ID | OrganizationId | CHAR(8) | 기관을 의미하는 Prefix ‘ORG’ 이후에 숫자 ‘0’으로 채워진 5자리 정수 Ex) ORG00000, ORG00001, ORG99999 | • 모든 기관은 반드시 Unique한 ID를 가져야 한다. |
| 시스템ID | SystemId | CHAR(8) | 시스템을 의미하는 Prefix ‘SYS’ 이후에 숫자 ‘0’으로 채워진 5자리 정수 Ex) SYS00000, SYS00001, SYS99999 | • 동일한 기관에 속한 모든 시스템은 반드시 Unique한 ID를 가져야 한다. (서로 다른 기관에 속한 시스템은 같은 ID를 가질 수 있다.) |
| 서비스ID | ServiceId | CHAR(8) | 서비스를 의미하는 Prefix ‘SRV’ 이후에 숫자 ‘0’으로 채워진 5자리 정수 Ex) SRV00000, SRV00001, SRV99999 | • 동일한 시스템에 속한 모든 서비스는 반드시 Unique한 ID를 가져야 한다. (서로 다른 시스템에 속한 시스템은 같은 ID를 가질 수 있다.) |
| 타입ID | TypeId | VARCHAR2(40) | 알파벳 대소문자, 숫자, ‘_’ 로 구성된 길이 1 ~ 40 사이의 문자열 Ex) messageType0000, record_ABC | • 모든 타입은 Unique한 ID를 가져야 한다. • 메타데이터로 저장되는 레코드타입의 ID는 다음의 Reserved 값을 가질 수 없다. boolean, string, byte, short, integer, long, biginteger, float, double, bigdecimal, calendar |
| 모듈ID | BeanId | VARCHAR2(40) | 알파벳 대소문자, 숫자, ‘_’ 로 구성된 길이 1 ~ 40 사이의 문자열 Ex) serviceProviderA, bean_123 | • 모듈ID는 전자정부 개발프레임워크의 기반이 되는 Spring Framework에 등록되는 Bean Id이다. |
연계 서비스 API는 연계 서비스를 사용 및 제공하기 위한 interface를 제공한다.
연계 서비스 API는 다음과 같이 구성된다.

| EgovIntegrationContext | 연계 서비스에 대한 설정 및 EgovIntegrationService 객체를 관리한다. |
| EgovIntegrationMessage | 연계 서비스를 통해 주고받는 표준 메시지를 정의한다. |
| EgovIntegrationMessageHeader | 연계 서비스를 통해 주고받는 표준 메시지 헤더를 정의한다. |
| EgovIntegrationMessageHeader::ResultCode | 연계 서비스 결과 코드를 담고 있는 enumeration이다. |
| EgovIntegrationService | 연계 서비스를 호출하기 위해 사용한다. |
| EgovIntegrationResponse | 연계 서비스를 비동기 방식으로 호출한 경우, 응답 메시지를 받기 위해 사용한다. |
| EgovIntegrationServiceCallback | 연계 서비스를 비동기 방식으로 호출한 경우, 응답 메시지를 받기 위한 Callback interface이다. |
| EgovIntegrationServiceCallback::CallbackId | 연계 서비스를 Callback을 이용한 비동기 방식으로 호출한 경우, 요청 메시지와 응답 메시지를 연결하기 위한 ID를 나타내는 interface이다. |
| EgovIntegrationServiceProvider | 연계 서비스를 제공하기 위해 사용한다. |
EgovIntegrationContext는 연계 서비스에 대한 설정 및 EgovIntegrationService 객체를 관리한다. 연계 서비스를 사용하기 위해서는 EgovIntegrationContext의 getService 메소드를 사용하여 EgovIntegrationService 객체를 얻어와야 한다.
아래는 주민등록번호와 성명을 이용하여 실명확인을 수행하는 예제이다.
package itl.sample;
import javax.annotation.Resource;
import egovframework.rte.itl.integration.EgovIntegrationContext;
import egovframework.rte.itl.integration.EgovIntegrationService;
public class EgovIntegrationSample
{
@Resource(name = "egovIntegrationContext")
private EgovIntegrationContext egovIntegrationContext;
public boolean verifyName(final String name, final String residentRegistrationNumber)
{
// 연계ID가 "INT_VERIFY_NAME"인 연계 서비스 객체를 얻어온다.
EgovIntegrationService service = egovIntegrationContext.getService("INT_VERIFY_NAME");
// 요청 메시지 생성
EgovIntegrationMessage requestMessage = service.createRequestMessage();
// 요청 메시지 작성
requestMessage.getBody().put("name", name);
requestMessage.getBody().put("residentRegistrationNumber", residentRegistrationNumber);
// 서비스 요청
EgovIntegrationMessage responseMessage = service.sendSync(requestMessage);
// 결과 return
return responseMessage.getBody().get("result");
}
}
위 예제에 해당하는 Metadata는 아래와 같다.
| INTEGRATION | ||||||
| ID | PROVIDER_SERVICE_KEY | CONSUMER_SYSTEM_KEY | DEFAULT_TIMEOUT | USING_YN | VALIDATE_FROM | VALIDATE_TO |
| 'INT_VERIFY_NAME' | 'SERVICE_VERIFY_NAME' | 'SYSTEM_CONSUMER' | 5000 | 'Y' | NULL | NULL |
| ORGANIZATION | |
| ID | NAME |
| 'ORG00001' | '요청 기관' |
| 'ORG00002' | '제공 기관' |
| SYSTEM | ||||
| SYSTEM_KEY | ORGANIZATION_ID | SYSTEM_ID | SYSTEM_NAME | STANDARD_YN |
| 'SYSTEM_CONSUMER' | 'ORG00001' | 'SYS00001' | '요청 시스템' | 'Y' |
| 'SYSTEM_PROVIDER' | 'ORG00002' | 'SYS00001' | '응답 시스템' | 'Y' |
| SERVICE | ||||||||
| SERVICE_KEY | SYSTEM_KEY | SERVICE_ID | SERVICE_NAME | REQUEST_MESSAGE_TYPE_ID | RESPONSE_MESSAGE_TYPE_ID | SERVICE_PROVIDER_BEAN_ID | USING_YN | STANDARD_YN |
| 'SERVICE_VERIFY_NAME' | 'SYSTEM_PROVIDER' | 'SRV00001' | 'VerifyName' | 'REQ_VERIFY_NAME' | 'RES_VERIFY_NAME' | 'serviceVerifyName' | 'Y' | 'Y' |
| RECORD_TYPE | ||
| RECORD_TYPE_ID | RECORD_TYPE_NAME | PARENT_RECORD_TYPE_ID |
| 'REQ_VERIFY_NAME' | 'RequestVerifyName' | NULL |
| 'RES_VERIFY_NAME' | 'ResponseVerifyName' | NULL |
| RECORD_TYPE_FIELD | ||
| RECORD_TYPE_ID | RECORD_FIELD_NAME | RECORD_FIELD_TYPE_ID |
| 'REQ_VERIFY_NAME' | 'name' | 'string' |
| 'REQ_VERIFY_NAME' | 'residentRegistrationNumber' | 'string' |
| 'RES_VERIFY_NAME' | 'result' | 'boolean' |
EgovIntegrationMessage는 헤더부, 바디부, 첨부파일로 구성된다.
EgovIntegrationMessage의 헤더부를 access 하기 위한 메소드는 아래와 같다.
| Method Summary | |
| EgovIntegrationMessageHeader | getHeader() |
| void | setHeader(EgovIntegrationMessageHeader header) |
EgovIntegrationMessage의 바디부를 access 하기 위한 메소드를 아래와 같다.
| Method Summary | |
| Map<String, Object> | getBody() |
| void | setBody(Map<String, Object> body) |
EgovIntegrationMessage의 바디부는 다음 값들로만 구성될 수 있다.
EgovIntegrationMessage의 첨부파일을 access 하기 위한 메소드는 아래와 같다.
| Method Summary | ||||
| Map<String, Object> | getAttachments() | |||
| void | setAttachments(Map<String, Object> attachments) | |||
| Object | getAttachment(String name) | |||
| void | putAttachment(String name, Object attachment) | |||
| Object | removeAttachment(String name) | |||
EgovIntegrationMessageHeader는 다음과 같은 정보를 담고 있다.
| IntegrationId | String | 연계ID |
| ProviderOrganizationId | String | 연계 제공 기관ID |
| ProviderSystemId | String | 연계 제공 시스템ID |
| ProviderServiceId | String | 연계 제공 서비스ID |
| ConsumerOrganizationId | String | 연계 요청 기관ID |
| ConsumerSystemId | String | 연계 요청 시스템ID |
| RequestSendTime | Calendar | 요청 송신 시각 |
| RequestReceiveTime | Calendar | 요청 수신 시각 |
| ResponseSendTime | Calendar | 응답 송신 시각 |
| ResponseReceiveTime | Calendar | 응답 수신 시각 |
| ResultCode | ResultCode | 결과 코드 |
| ResultMessage | String | 결과 메시지 |
EgovIntegrationMessageHeader는 위 attribute에 대한 get/set 메소드를 정의하고 있다.
EgovIntegrationMessageHeader의 ResultCode Attribute는 다음 연계 서비스 결과 코드 중 하나의 값을 담고 있다.
| OK | 정상 종료 | “0000” | 연계가 정상적으로 종료된 경우 |
| TIME_OUT | Timeout 발생 | “0001” | 연계 수행 중 Client 단에서 Timeout이 발생한 경우 |
| BUSINESS_ERROR | 업무 오류 발생 | “0002” | 연계 수행 중 Server 단에서 업무적인 오류가 발생한 경우 |
| NOT_USABLE_INTEGRATION | 사용하지 않는 연계 | “1000” | 연계 정의(IntegrationDefinition)의 using flag가 false인 경우 |
| INVALID_TIME | 연계 가용시간이 아님 | “1001” | 연계를 요청한 시각이 연계 정의(IntegrationDefinition)의 validateFrom과 validateTo 사이가 아닌 경우 • 연계 가용시각 조건 = (validateFrom == null || validateFrom.compareTo(now) <= 0) && (validateTo == null || now.compareTo(validateTo) <= 0)* now : Calendaer = 현재 시각 |
| NOT_USABLE_SERVICE | 사용하지 않는 서비스 | “1002” | 연계 정의(IntegrationDefinition)에 등록된 제공 서비스(ServiceDefinition)의 using flag 값이 false인 경우 |
| UNEXPECTED_CONSUMER | 기대하지 않은 연계 요청자 | “1003” | 연계 제공 시스템(Server)에 등록된 연계 정의(IntegrationDefinition)의 요청 시스템 코드값과 요청 메시지 헤더의 요청 시스템 코드값이 일치하지 않는 경우 |
| UNEXPECTED_PROVIDER | 기대하지 않은 연계 제공자 | “1004” | 연계 제공 시스템(Server)에 등록된 시스템 코드와 요청 메시지 헤더의 제공 시스템 코드값이 일치하지 않는 경우 |
| NO_SUCH_SERVICE | 제공하지 않는 서비스 | “1005” | 연계 제공 시스템(Server)에 등록되어 있지 않은 서비스를 요청한 경우 |
| FAIL_IN_INITIALIZING | 연계 서비스 초기화 실패 | “1006” | 연계 제공 서비스를 초기화하는데 실패한 경우 |
| FAIL_IN_CREATING_REQUEST_MESSAGE | 요청 메시지 생성 실패 | “2000” | Client에서 요청 메시지를 생성할 때 오류가 발생한 경우 |
| FAIL_IN_SENDING_REQUEST | 요청 메시지 송신 실패 | “2001” | Client에서 요청 메시지를 송신할 때 오류가 발생한 경우 |
| FAIL_IN_RECEIVING_REQUEST | 요청 메시지 수신 실패 | “3000” | Server에서 요청 메시지를 수신할 때 오류가 발생한 경우 |
| FAIL_IN_PARSING_REQUEST_MESSAGE | 요청 메시지 분석 실패 | “3001” | Server에서 요청 메시지를 분석할 때 오류가 발생한 경우 |
| NO_MESSAGE_HEADER_IN_REQUEST | 요청 메시지 헤더 부재 | “3002” | Server에서 받은 요청 메시지에 표준 메시지 헤더가 존재하지 않는 경우 |
| FAIL_TO_CALL_SERVICE_PROVIDER | 서비스 제공 모듈 호출 실패 | “3003” | Server에서 서비스 제공 모듈을 호출할 때 오류가 발생한 경우 |
| FAIL_IN_CREATING_RESPONSE_MESSAGE | 응답 메시지 생성 실패 | “4000” | Server에서 응답 메시지를 생성할 때 오류가 발생한 경우 |
| FAIL_IN_SENDING_RESPONSE | 응답 메시지 송신 실패 | “4001” | Server에서 응답 메시지를 송신할 때 오류가 발생한 경우 |
| FAIL_IN_RECEIVING_RESPONSE | 응답 메시지 수신 실패 | “5000” | Client에서 응답 메시지를 수신할 때 오류가 발생한 경우 |
| FAIL_IN_PARSING_RESPONSE_MESSAGE | 응답 메시지 분석 실패 | “5001” | Client에서 응답 메시지를 분석할 때 오류가 발생한 경우 |
EgovIntegrationService를 동기화 방식의 호출과 비동시화 방식의 호출을 지원한다.
EgovIntegrationService의 sendSync 메소드는 동기화 방식으로 연계 서비스를 호출한다.

...
public class EgovIntegrationSample
{
...
public boolean verifyName(final String name, final String residentRegistrationNumber)
{
// EgovIntegrationContext에서 EgovIntegrationService 객체를 얻어온 후, 요청 메시지를 생성 및 작성한다.
...
// 동기방식으로 연계 서비스 호출 (timeout = 5000 millisecond)
EgovIntegrationMessage responseMessage = service.sendSync(requestMessage, 5000);
// 응답 결과 처리
...
}
...
}
EgovIntegrationContext 또는 Metadata의 연계등록정보에 등록된 default timeout 값을 사용할 경우, timeout 값을 생략할 수 있다.
...
EgovIntegrationMessage responseMessage = servicd.sendSync(requestMessage);
...
EgovIntegrationService의 sendAsync 메소드는 비동기화 방식으로 연계 서비스를 호출한다. sendAsync 메소드는 두가지 방식이 존재한다.
EgovIntegrationServiceResponse를 이용한 비동기 호출 방식이다. Response 방식의 비동기 호출은 연계 서비스를 요청하는 업무 모듈에 응답에 대한 ownership를 가지고 있으며, 응답 결과를 스스로 처리해야 하는 경우 사용한다.

...
public class EgovIntegrationSample
{
...
public boolean verifyName(final String name, final String residentRegistrationNumber)
{
// EgovIntegrationContext에서 EgovIntegrationService 객채를 얻어온 후, 요청 메시지를 생성 및 작성한다.
...
// 비동기방식으로 연계 서비스 호출
EgovIntegrationServiceResponse response = service.sendAsync(requestMessage);
// response 객체를 이용하여 응답 메시지를 받기 전에 필요한 업무를 수행
...
// response 객체를 이용하여 응답 메시지 수신(timeout = 5000 millisecond)
EgovIntegrationMessage responseMessage = response.receive(5000);
// 응답 메시지 처리
...
}
...
}
EgovIntegrationContext 또는 Metadata의 연계등록정보에 등록된 default timeout 값을 사용할 경우, timeout 값을 생략할 수 있다.
...
// response 객체를 이용하여 응답 메시지 수신
EgovIntegrationMessage responseMessage = response.receive();
...
EgovIntegrationServiceCallback를 이용한 비동기 호출 방식이다. Callback 방식의 비동기 호출은 연계 서비스를 요청하는 업무 모듈은 단지 요청만을 수행하고, 응답에 대한 처리는 Callback 객체에게 위임해도 상관없은 경우 사용한다.

...
public class EgovIntegrationSample
{
...
@Resource(name = "verifyNameServiceCallback")
private EgovIntegrationServiceCallback callback;
public boolean verifyName(final String name, final String residentRegistrationNumber)
{
// EgovIntegrationContext에서 EgovIntegrationService 객채를 얻어온 후, 요청 메시지를 생성 및 작성한다.
...
// 비동기방식으로 연계 서비스 호출
service.sendSync(requestMessage, callback);
}
...
}
package itl.sample;
import egovintegration.rte.itl.integration.EgovIntegrationServiceCallback;
import egovintegration.rte.itl.integration.EgovIntegrationServiceCallback.CallbackId;
public class VefiryNameServiceCallback
{
public CallbackId createId(EgovIntegrationService service, EgovIntegrationMessage requestMessage)
{
// 본 메소드는 EgovIntegrationService를 구현한 연계 Adaptor 또는 솔루션에서 불리워진다.
// 서비스와 요청 메시지를 이용하여 CallbackId를 생성하여 return한다.
// 생성한 CallbackId는 응답 메시지 수신 시, 해당하는 서비스 및 요청 메시지를 식별하기 위해 사용한다.
// CallbackId 생성
CallbackId callbadkId = ...
return callbackId;
}
public vod onReceive(CallbackId callbackId, EgovIntegrationMessage responseMessage)
{
// 본 메소드는 처리해야 하는 응답 메시지가 도착했을 때, 연계 Adaptor 또는 솔루션에 의해 불리워진다.
// 응답 메시지 처리
...
}
}
EgovIntegrationServiceProvider interface는 연계 서비스를 제공하기 위한 interface로 연계 서비스를 제공하는 모듈은 본 interface를 implements 해야 한다.
아래 예제는 이름과 주민등록번호는 이용하여 실명확인을 수행하는 서비스를 제공하는 업무 모듈과 Spring Framework Configuration XML 파일이다. (Metadata는 EgovIntegrationContext 예제의 설정과 같다.)
package itl.sample;
import egovframework.rte.itl.integration.EgovIntegrationMessage;
import egovframework.rte.itl.integration.EgovIntegrationServiceProvider;
public class ServiceVerifyName implements EgovIntegrationServiceProvider
{
public void service(EgovIntegrationMessage requestMessage, EgovIntegrationMessage responseMessage)
{
String name = requestMessage.getBody().get("name");
String residentRegistrationNumber = requestMessage.getBody().get("residentRegistrationNumber");
// 실명 확인
boolean result = varifyName(name, residentRegistrationNumber);
responseMessage.getBody().put("result", result);
}
}
...
<bean id="serviceVerifyName" class="itl.sample.ServiceVerifyName"/>
...
WebService는 전자정부 개발프레임워크 Integration 서비스 표준에 따라 WebService를 요청하고 제공하기 위한 Library이다.
W3C는 Web Service를 “네트워크 상에서 발생하는 컴퓨터 간의 상호작용을 지원하기 위한 소프트웨어 시스템”으로 정의하고 있다. 일반적으로 Web Service는 인터넷과 같은 네트워크 상에서 접근되고, 요청된 서비스를 제공하는 원격 시스템에서 수행되는 Web APIs이다.

WebService는 Web Service 구현하기 위해서 Apache CXF를 사용한다.
WebService는 Integration Service 표준에 따라 구현한 Library이므로, 본 장에서는 API 등의 사용 방식은 설명하지 않는다. 본 장은 WebService 만을 위한 추가적인 설정 정보를 설명하고, 설정 방법을 가이드한다.
WebService는 연계 서비스를 요청하고 제공하기 위한 Web Service Client와 Server 정보를 필요로한다.

| WEB_SERVICE_SERVER | 연계 서비스를 Web Service 형태로 공개(publish)하기 위해 필요한 정보를 담고 있다. |
| WEB_SERVICE_CLIENT | Web Service 형태로 공개(publish)되어 있는 연계 서비스를 호출하기 위해 필요한 정보를 담고 있다. |
| WEB_SERVICE_MAPPING | 전자정부 Integration 서비스 표준에 따라 개발된 서비스가 아닌 기존의 Legacy 시스템의 Web Service를 호출하기 위해, 표준 메시지와 Web Service 메시지 간의 mapping 정보를 담고 있다. |
| URL | VARCHAR2(200) | URL을 나타낸다. | |
| WebServiceMappingType | CHAR(2) | Req/Res 구분을 나타낸다. | ‘REQ’ : Request ‘RES’ : Response |
| Table 명 | WEB_SERVICE_SERVER | ||||||
| 설명 | 연계 서비스를 Web Service 형태로 공개(publish)하기 위해 필요한 정보를 담고 있다. | ||||||
| Column | |||||||
| Seq | PK | Column명 | 한글명 | Domain | Data Type | Null | 설명 |
| 1 | Y | SERVICE_KEY | 서비스KEY | SurrogateKey | VARCHAR2(20) | N | 서비스 Key이다. |
| 2 | ADDRESS | 주소 | URL | VARCHAR2(200) | N | 서비스를 공개할 주소이다. | |
| 3 | NAMESPACE | 네임스페이스 | URL | VARCHAR2(200) | N | 서비스의 네임스페이스이다. | |
| 4 | SERVICE_NAME | 서비스명 | Name | VARCHAR2(40) | N | 공개할 때 사용할 서비스의 이름이다. | |
| 5 | PORT_NAME | 포트명 | Name | VARCHAR2(40) | N | 공개할 때 사용할 포트의 이름이다. | |
| 6 | OPERATION_NAME | 기능명 | Name | VARCHAR2(40) | N | 공개할 때 사용할 기능의 이름이다. | |
| Constraints | |||||||
| PRIMARY KEY (SERVICE_KEY) | |||||||
| FOREIGN KEY (SERVICE_KEY) REFERENCES SERVICE (SERVICE_KEY) | |||||||
| Table 명 | WEB_SERVICE_CLIENT | ||||||
| 설명 | Web Service 형태로 공개(publish)되어 있는 연계 서비스를 호출하기 위해 필요한 정보를 담고 있다. | ||||||
| Column | |||||||
| Seq | PK | Column명 | 한글명 | Domain | Data Type | Null | 설명 |
| 1 | Y | SERVICE_KEY | 서비스KEY | SurrogateKey | VARCHAR2(20) | N | 서비스 Key이다. |
| 2 | WSDL_ADDRESS | WSDL 주소 | URL | VARCHAR2(200) | N | 사용할 서비스의 WSDL 주소이다. | |
| 3 | NAMESPACE | 네임스페이스 | URL | VARCHAR2(200) | N | 서비스의 네임스페이스이다. | |
| 4 | SERVICE_NAME | 서비스명 | Name | VARCHAR2(40) | N | 사용할 서비스의 이름이다. | |
| 5 | PORT_NAME | 포트명 | Name | VARCHAR2(40) | N | 사용할 포트의 이름이다. | |
| 6 | OPERATION_NAME | 기능명 | Name | VARCHAR2(40) | N | 사용할 기능의 이름이다. | |
| Constraints | |||||||
| PRIMARY KEY (SERVICE_KEY) | |||||||
| FOREIGN KEY (SERVICE_KEY) REFERENCES SERVICE (SERVICE_KEY) | |||||||
| Table 명 | WEB_SERVICE_MAPPING | ||||||
| 설명 | 전자정부 Integration 서비스 표준에 따라 개발된 서비스가 아닌 기존의 Legacy 시스템의 Web Service를 호출하기 위해, 표준 메시지와 Web Service 메시지 간의 mapping 정보를 담고 있다. | ||||||
| Column | |||||||
| Seq | PK | Column명 | 한글명 | Domain | Data Type | Null | 설명 |
| 1 | Y | SERVICE_KEY | 서비스KEY | SurrogateKey | VARCHAR2(20) | N | 서비스 Key이다. |
| 2 | Y | MESSAGE_TYPE | 메시지타입 | WebServiceMappingType | CHAR(3) | N | Req/Res 구분이다. |
| 3 | Y | FIELD_NAME | 필드명 | Name | VARCHAR2(40) | N | 표준 메시지 Field 이름이다. |
| 4 | ARGUMENT_INDEX | 변수순서 | Number | Integer | N | Web Service 메시지의 변수 순서이다. | |
| 5 | ARGUMENT_NAME | 변수명 | Name | VARCHAR2(40) | N | Web Service 메시지의 변수 이름이다. | |
| 6 | HEADER_YN | 헤더여부 | Boolean | CHAR(1) | N | Web Service 헤더 여부이다. | |
| Constraints | |||||||
| PRIMARY KEY (SERVICE_KEY, MESSAGE_TYPE, FIELD_NAME) | |||||||
| FOREIGN KEY (SERVICE_KEY) REFERENCES WEB_SERVICE_CLIENT (SERVICE_KEY) | |||||||
WebService를 사용하기 위해 다음의 설정이 필요하다.
WebService를 사용하기 위해서 pom.xml의 dependencies tag에 다음 dependency를 추가한다.
...
<dependencies>
...
<dependency>
<groupId>egovframework.rte</groupId>
<artifactId>egovframework.rte.itl.webservice</artifactId>
<version>${egovframework.version}</version>
</dependency>
...
</dependencies>
...
WebService를 위한 기본적인 설정이 포함된 “context-webservice.xml” 파일을 Spring XML Configuration 파일에 import한다.
<import resource="classpath:/egovframework/rte/itl/webservice/context/context-webservice.xml"/>
그리고 Context와 DataSource를 등록해야 한다.(DataSource의 경우, 프로젝트에서 사용하는 것이 있을 경우 설정하지 않아도 된다. 단, 반드시 id가 “dataSource”이여야 한다.)
<!-- EgovWebServiceContext 이다.
organizationId 와 systemId 는 현재 시스템의 기관ID 및 시스템ID를 넣어야 한다. -->
<bean id="egovWebServiceContext"
class="egovframework.rte.itl.webservice.EgovWebServiceContext"
init-method="init">
<property name="organizationId" value="ORG_EGOV"/>
<property name="systemId" value="SYS00001"/>
<property name="defaultTimeout" value="5000"/>
<property name="integrationDefinitionDao" ref="integrationDefinitionDao"/>
<property name="webServiceServerDefinitionDao" ref="webServiceServerDefinitionDao"/>
<property name="webServiceClientDefinitionDao" ref="webServiceClientDefinitionDao"/>
<property name="typeLoader" ref="typeLoader"/>
<property name="classLoader" ref="classLoader"/>
</bean>
<!-- DataSource 설정이다. 시스템에 맞게 재작성 해야 한다. 아래는 HSQL Sample이다. -->
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
<property name="driverClassName" value="net.sf.log4jdbc.DriverSpy"/>
<property name="url" value="jdbc:log4jdbc:hsqldb:hsql://localhost/test"/>
<property name="username" value="sa"/>
<property name="password" value=""/>
<property name="defaultAutoCommit" value="false"/>
<property name="poolPreparedStatements" value="true"/>
</bean>
WebService Client 모듈은 Web Service로 공개된 Integration 서비스 표준에 따라 호출하는 모듈로서, 본 장은 설정 방식을 설명한다. (호출 방식은 연계 서비스 API를 참조한다.)
Client 모듈을 설정하기 위해서는 다음 과정이 필요하다.
Client 모듈을 설정하기 위해서는 Metadata의 WEB_SERVICE_CLIENT Table에 설정을 추가해야 한다.
다음과 같이 Integration 서비스의 Metadata인 INTEGRATION Table에 연계등록정보가 설정되어 있다고 가정한다. (* 기관, 시스템, 서비스, 메시지타입 등의 정보는 설정되어 있으며, 개발하는 시스템은 ‘SYSTEM_CONSUMER’라고 가정함)
| INTEGRATION | ||||||
| ID | PROVIDER_SERVICE_KEY | CONSUMER_SYSTEM_KEY | DEFAULT_TIMEOUT | USING_YN | VALIDATE_FROM | VALIDATE_TO |
| 'INT_VERIFY_NAME' | 'SERVICE_VERIFY_NAME' | 'SYSTEM_CONSUMER' | 5000 | 'Y' | NULL | NULL |
Web Service ‘SERVICE_VERIFY_NAME’를 호출하기 위해서 WEB_SERVICE_CLIENT에 ‘SERVICE_VERIFY_NAME’을 SERVICE_KEY로 갖는 설정을 추가해야 한다.
| WEB_SERVICE_CLIENT | |||||
| SERVICE_KEY | WSDL_ADDRESS | NAMESPACE | SERVICE_NAME | PORT_NAME | OPERATION_NAME |
| 'SERVICE_VERIFY_NAME' | 'http://192.168.0.1:8080/Sample/services/VerifyName?wsdl' | 'http://itl/sample/' | 'VerifyNameService' | 'VerifyNamePort' | 'service' |
만약 호출하는 Web Service가 전자정부 Integration 서비스 표준에 따라 개발된 서비스가 아닌 경우, 메시지 헤더부가 다를 수 있어 별도의 Mapping 정보가 필요하다.
전자정부 Integration 서비스 표준은 Web Service Header부에 들어갈 Attribute들이 EgovIntegrationMessageHeader에 정의되어 있고, 바디부는 EgovIntegrationMessage의 body에 정의되어 있으므로 별도의 mapping 정보 없이 header와 body 부의 구분이 가능하지만, 표준을 따르지 않은 Web Service의 경우 EgovIntegrationMessage의 body부에 정의되어 있는 일부 값들을 헤더에 포함시켜야 한다.
WEB_SERVICE_MAPPING Table의 정보는 Integration 서비스 표준에 정의되어 있는 메시지 형태를 기준으로 한다. 서비스 ‘SERVICE_VERIFY_NAME’의 Request Message는 ’name’, ‘residentRegistrationNumber’ 필드를 가지고, Response Message는 ‘result’ 필드를 가진다. 따라서 ‘SERVICE_VERIFY_NAME’에 해당하는 WEB_SERVICE_MAPPING은 다음의 정보를 가져야 한다.
| WEB_SERVICE_MAPPING | |||||
| SERVICE_KEY | MESSAGE_TYPE | FIELD_NAME | ARGUMENT_INDEX | ARGUMENT_NAME | HEADER_YN |
| 'SERVICE_VERIFY_NAME' | 'REQ' | 'name' | 1 | 'name' | Y |
| 'SERVICE_VERIFY_NAME' | 'REQ' | 'residentRegistrationNumber' | 2 | 'residentRegistrationNumber' | N |
| 'SERVICE_VERIFY_NAME' | 'RES' | 'result' | 1 | 'result' | N |
위 정보 중 HEADER_YN column의 값에 따라 해당 field가 Web Service Envelop의 header에 포함될지 여부를 판단한다. 위 설정값을 적용하면, 요청 메시지 중 ’name’ field는 Web Service Envelop의 헤더에 포함된다.
Web Service Server 모듈을 개발하는 과정은 다음과 같다.
web.xml에 EgovWebServiceServlet 설정을 추가한다.
...
<servlet>
<description></description>
<display-name>EgovWebServiceServlet</display-name>
<servlet-name>EgovWebServiceServlet</servlet-name>
<servlet-class>egovframework.rte.itl.webservice.EgovWebServiceServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>EgovWebServiceServlet</servlet-name>
<url-pattern>/services/*</url-pattern>
</servlet-mapping>
...
<url-pattern> tag의 값은 변경 될 수 있다. 자세한 설명은 다음 WEB_SERVICE_SERVER 설정을 참조한다.
다음과 같이 Integration 서비스의 Metadata인 INTEGRATION Table에 연계등록정보가 설정되어 있다고 가정한다. (* 기관, 시스템, 서비스, 메시지타입 등의 정보는 설정되어 있으며, 공개할 서비스는 ‘SERVICE_VERIFY_NAME’이라고 가정함)
| INTEGRATION | ||||||
| ID | PROVIDER_SERVICE_KEY | CONSUMER_SYSTEM_KEY | DEFAULT_TIMEOUT | USING_YN | VALIDATE_FROM | VALIDATE_TO |
| 'INT_VERIFY_NAME' | 'SERVICE_VERIFY_NAME' | 'SYSTEM_CONSUMER' | 5000 | 'Y' | NULL | NULL |
Web Service ‘SERVICE_VERIFY_NAME’를 공개하기 위해서 WEB_SERVICE_SERVER에 ‘SERVICE_VERIFY_NAME’을 SERVICE_KEY로 갖는 설정을 추가해야 한다.
| WEB_SERVICE_SERVICE | |||||
| SERVICE_KEY | ADDRESS | NAMESPACE | SERVICE_NAME | PORT_NAME | OPERATION_NAME |
| 'SERVICE_VERIFY_NAME' | '/VerifyName' | 'http://itl/sample/' | 'VerifyNameService' | 'VerifyNamePort' | 'service' |
<servlet-mapping> tag의 <url-pattern> tag의 값은 서비스를 제공하기 위한 주소로, WEB_SERVICE_SERVER Table의 ADDRESS Column 값은 <url-pattern> tag값에 대한 상대 위치를 나타낸다.
예를 들어, Web Application의 IP가 192.168.0.1, Port가 8080, Context Root가 “Sample”, url-patterns이 ”/services/*”인 경우, 위 ‘SERVICE_VERIFY_NAME’의 WSDL Address는 http://192.168.0.1:8080/Sample/services/VerifyName?wsdl이다.
전자정부 WebService를 포함한 어플리케이션을 WAS에 배포(deploy)하는 방법을 설명한다. Apache CXF의 경우 Web Service 관련 라이브러리를 CXF에서 제공하는 것을 사용해야 한다. 만약 WAS가 기본적으로 Web Service 라이브러리를 제공할 경우, 정상적으로 동작하지 않을 수 있다. 따라서 CXF 라이브러리를 사용할 수 있도록 설정을 변경해야 하는데, 대부부의 해결책은 Web Application의 WEB-INF의 라이브러리를 먼저 loading하도록 Class Loading 순서를 변경하는 것이다.
본 WebService는 JAX-WS 2.0 이상을 사용한다.
전자정부 WebService의 경우 내부적으로 CXF를 사용하지만 JEUS 6.0에 배포했을 경우 Server 모듈을 공개(publish)할 때 문제가 발생한다. 그 원인은 JEUS 6.0에 기본적으로 포함되어 있는 Web Services 관련 library와 전자정부 WebService가 사용하는 library가 같지 않기 때문이다. 현재 아래와 같은 2가지 문제가 발견되었다.
Publish Address 문제
Server 모듈을 publish할 때 EgovWebServiceServlet의 path에 대한 상대경로를 사용한다. Apache CXF가 사용하는 library의 경우, 이를 실제 주소로 변환해주지만, JEUS 6.0에 기본적으로 포함된 library는 그렇지 않기 때문에 IllegalArgumentException을 발생시킨다.
Service Endpoint Interface 참조 문제
전자정부 WebService는 Integration 서비스 표준에 따라 Server 모듈의 Service Endpoint Interface와 구현 class를 동적으로 생성한다. 하지만 JEUS 6.0에 기본적으로 포함된 library의 경우, 이렇게 동적으로 생성된 class를 인식하지 못해서 Exception이 발생한다.
해결방법은 전자정부 WebService가 사용하는 library가 ClassLoader에서 먼저 loading되게 하는 것이다. JEUS 6.0은 jeus-web-dd.xml 설정을 통해서 WEB-INF/lib에 있는 library를 먼저 loading하도록 설정할 수 있다.
<?xml version="1.0" encoding="UTF-8"?>
<jeus-web-dd xmlns="http://www.tmaxsoft.com/xml/ns/jeus" version="6.0">
<webinf-first>true</webinf-first>
</jeus-web-dd>
위 jeus-web-dd.xml 파일을 web.xml 파일이 존재하는 WEB-INF 폴더에 위치시킨다. 그리고, webinf-first를 true로 설정하는 경우, XML Parser에 대한 충돌이 발생한다. 충돌을 해결하기 위해서 아래 2개의 파일을 WEB-INF/lib에서 제거해야 한다.
JBoss의 경우, 아래 jboss-web.xml 파일을 추가한다.
<?xml version="1.0" encoding="UTF-8"?>
<jboss-web>
<class-loading java2ClassLoadingCompliance="false">
<loader-repository>
apache.cxf:archive=<WAR 파일명>
<loader-repository-config>
java2ParentDelegation=false
</loader-repository-config>
</loader-repository>
</class-loading>
</jboss-web>
*<WAR 파일명>은 deploy하는 war 파일명을 확장자를 포함하여 기재한다.
WebLogic 9.2 버전은 J2EE 1.4까지만 지원하므로, JAX-WS 2.0을 지원하지 않는다. WebService를 WebLogic에서 사용하기 위해서는 JAX-WS 2.0 이상을 지원하는 10.x 이상을 사용해야 한다.
Spring MVC를 통해 구현한 RESTful은 리소스에 대한 접근을 URI를 이용하며, HTTP의 PUT, GET, POST, DELETE 등과 같은 메소드의 의미를 그대로 사용하므로, 단순하게 접근 할 수 있다.
<servlet-mapping>
<servlet-name>action</servlet-name>
<url-pattern>*.html</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>action</servlet-name>
<url-pattern>*.xml</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>action</servlet-name>
<url-pattern>*.json</url-pattern>
</servlet-mapping>
<filter>
<filter-name>httpMethodFilter</filter-name>
<filter-class>org.springframework.web.filter.HiddenHttpMethodFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>httpMethodFilter</filter-name>
<url-pattern>/springrest/*</url-pattern>
</filter-mapping>
자세한 설명은 아래에 있다.
설정
REST 스타일의 URL은 ‘/cgr’, ‘/cgr/CATEGORY-00000000001’ 처럼 계층 구조로 사용가능하도록 설계되었다. 따라서 web.xml에 DispatcherServlet을 정의하고 매핑할 URL 패턴을 ‘/‘로 지정해야한다. DispatcherServlet URL 매핑 샘플은 다음과 같다.
<servlet-mapping>
<servlet-name>action</servlet-name>
<url-pattern>/springrest/*</url-pattern>
</servlet-mapping>
아래와 같은 방법으로도 DispatcherServlet URL 매핑을 사용 할 수 있다.
<servlet-mapping>
<servlet-name>action</servlet-name>
<url-pattern>*.do</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>action</servlet-name>
<url-pattern>*.html</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>action</servlet-name>
<url-pattern>*.xml</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>action</servlet-name>
<url-pattern>*.json</url-pattern>
</servlet-mapping>
사용
Spring에서 제공하는 REST 지원 기능들은 모두 Spring MVC 기반으로 되어 있다. REST 방식으로 노출되는 서비스는 곧 Controller의 메소드이기 때문에 기존에 웹 어플리케이션을 개발하던 방식과 크게 다르지 않다.
Resource의 ID인 URI를 Controller 클래스나 메소드에 매핑하기 위해서는 @RequestMapping을 사용한다. @RequestMapping이 URI Template을 지원하기 때문에 아래 샘플코드와 같이 사용할 수 있다.
@Controller
@SessionAttributes(types=CategoryVO.class)
public class EgovCategoryController {
//…
@RequestMapping(value="/springrest/cgr/{ctgryId}", method=RequestMethod.GET)
public String updtCategoryView(@PathVariable String ctgryId, Model model) throws Exception{
// …
}
}
모든 HTTP method 사용을 위해서 @RequestMapping에서 ‘method’ 속성을 제공한다. 따라서, ‘/springrest/cgr/CATEGORY-00000000001’이라는 URI가 GET으로 요청이 들어올 경우 위의 updtCategoryView ( ) 메소드가 매핑될 것이다.
@PathVariable annotation추가
‘/springrest/cgr/CATEGORY-00000000001’로 URI요청이 들어왔을 경우 @PathVariable을 사용하여 ‘ctgryID’ 입력 인자로 바인딩 된다.
@RequestMapping(value="/springrest/cgr/{ctgryId}", method=RequestMethod.GET)
public String updtCategoryView(@PathVariable String ctgryId, Model model) throws Exception{
// …
}
설정
브라우저 기반의 HTML에서는 GET, POST만 지원한다. 일반적으로 HTTP에서는 POST를 사용하고, hidden 타입의 입력값으로 HTTP METHOD를 지정하는 경우가 많다. 다음은 web.xml에 HiddenHttpMethodFilter를 정의한 모습이다.
<filter>
<filter-name>httpMethodFilter</filter-name>
<filter-class>org.springframework.web.filter.HiddenHttpMethodFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>httpMethodFilter</filter-name>
<url-pattern>/springrest/*</url-pattern>
</filter-mapping>
사용
web.xml에 HiddenHttpMethodFilter 설정을 추가하면, HTTP Method가 POST이고 _method라는 파라미터가 존재하는 경우 HTTP의 Method를 _method 값으로 바꾼다.
또한 Spring에서는 <form:form>에서 실제 HTTP Method를 지정하는 hidden 타입의 입력 필드를 자동으로 추가해주기 때문에 훨씬 더 편리하게 사용할 수 있다.
<form:form method="delete">
<input type=submit value=Delete/>
</form:form>
JSP에 위와 같이 작성하면, 내부적으로는 POST 방식으로 “_method=delete”가 전달되는 것이다.
샘플코드이다.
function fncSubmit(method) {
document.detailForm._method.value=method;
document.detailForm.submit();
}
//..
<form:form name="detailForm" method="${method}">
<a href="javascript:fncSubmit('delete');">삭제</a>
</form:form>
Xml과 json 등 다른 view로 보여지는 것으로 spring에서는 ContentNegotiatingViewResolver를 제공한다. ContentNegotiatingViewResolver는 다른 View Resolver들과 반드시 함께 사용되어야 하므로 View Resolver 설정 시 반드시 order를 정의해야 한다. 당연히 ContentNegotiatingViewResolver가 가장 높은 우선순위(가장 작은숫자)를 가져야 한다. defaultView는 View를 찾지 못한 경우 디폴트 View로 사용된다.
<bean class="org.springframework.web.servlet.view.ContentNegotiatingViewResolver">
<property name="defaultViews">
<list>
<bean class="org.springframework.web.servlet.view.json.MappingJacksonJsonView">
<property name="prefixJson" value="false"/>
</bean>
</list>
</property>
</bean>
MarshallingView
클라이언트에게 xml 응답을 돌려주기 위해 Spring OXM Marshaller를 사용한다. Spring oxm는 JAXB2, XMLBeans, JiBX, Castor등을 사용하여 Marshaller를 손쉽게 정희할 수 있게 해준다. Restful 예제에서는 JAXB2를 사용하였다. (OXM예제는 Castor사용)
<bean class="org.springframework.web.servlet.view.ContentNegotiatingViewResolver">
<property name="mediaTypes">
<map>
<entry key="html" value="text/html" />
<entry key="xml" value="application/xml" />
<entry key="json" value="application/json" />
</map>
</property>
<property name="order" value="0" />
//..
</beans>
<bean name="cgr/egovCategoryRegister" class="org.springframework.web.servlet.view.xml.MarshallingView">
<property name="marshaller" ref="marshaller" />
</bean>
<oxm:jaxb2-marshaller id="marshaller">
<oxm:class-to-be-bound name="egovframework.rte.tex.cgr.service.CategoryVO" />
</oxm:jaxb2-marshaller>
MappingJacksonJsonView
JSON으로 응답을 전달할 수 있는 View.
<bean class="org.springframework.web.servlet.view.ContentNegotiatingViewResolver">
<property name="mediaTypes">
<map>
<entry key="html" value="text/html" />
<entry key="xml" value="application/xml" />
<entry key="json" value="application/json" />
</map>
</property>
<property name="order" value="0" />
//..
</beans>
<bean name="cgr/egovCategoryList"
class="org.springframework.web.servlet.view.json.MappingJacksonJsonView" />
jsp
function fncSubmit(method) {
document.detailForm._method.value=method;
document.detailForm.submit();
}
//..
<form:form name="detailForm" method="${method}">
//..
</form:form>
//..
<a href="javascript:fncSubmit('post');">등록</a> //----------- controller 2.
<a href="javascript:fncSubmit('put');">수정</a> //------------- controller 3.
<a href="javascript:fncSubmit('delete');">삭제</a> //----------- controller 4.
<a href=" /springrest/cgr/{id}.xml">xml 보기</a> // ContentNegotiatingViewResolver설정
<a href=" /springrest/cgr/{id}.json">json(defaultView) 보기</a> // ContentNegotiatingViewResolver설정
<a href=" /springrest/cgr.html">목록</a> //------------------ controller 1.
<a href=" /springrest/cgr.json">목록(json)</a> // ContentNegotiatingViewResolver 설정
controller
// 1. 목록
@RequestMapping(value="/springrest/cgr", method=RequestMethod.GET)
public String selectCategoryList(..) throws Exception {
//..
}
// 2. 등록
@RequestMapping(value="/springrest/cgr", method = RequestMethod.POST, ..)
public String create( ..) throws Exception {
//..
}
// 3. 수정
@RequestMapping(value = "/springrest/cgr/{ctgryId}", method = RequestMethod.PUT, ..)
public String update(..) throws Exception {
//..
}
// 4. 삭제
@RequestMapping(value = "/springrest/cgr/{ctgryId}", method=RequestMethod.DELETE)
public String deleteCategory(@PathVariable String ctgryId, SessionStatus status) throws Exception{
//..
}
Spring Cloud Stream은 공유 메시징 시스템과 연결된 확장성이 뛰어난 이벤트 기반 마이크로서비스를 구축하기 위한 프레임워크이다.
Spring Cloud Stream의 핵심 구성 요소는 다음과 같다.
Spring Cloud Stream은 Spring Integration의 메시지 처리 핵심 기능을 기반으로 사용한다.
또한 Spring Boot를 기반으로 Binder 구현체를 제공하여 메시지 처리를 추상화 하여 동일 환경 뿐만 아니라 이기종의 시스템 또는 다른 환경 간에도 연계 메시지 처리를 지원한다.
비동기 데이터 처리는 지속적으로 발생하는 데이터에 대하여 실시간으로 처리 하는데 주요 목적이 있으며, 시간에 비교적 민감한 자료의 처리에 적합하며 다양한 지리적 위치에서 다양한 형식으로 전달될 수 있다.

| 배치 처리 | 비동기 데이터 처리 |
|---|---|
| 한정된 대량의 데이터 | 지속적으로 데이터가 발생 |
| 스케줄러를 사용하여 특정 시간에 처리 | 데이터 발생주기는 일정한 경우와 불규칙한 경우 모두 가능 |
| 일괄로 정해진 묶음단위 처리 | 데이터를 실시간으로 처리 |
java.util.function 패키지의 Functional Interface를 기반으로 람다식 사용 시 Supplier, Function, Consumer를 활용하여 클래스를 생성하지 않고 구현이 가능하다.
이 경우 Supplier는 Sink Binding으로 1초마다 주기적으로 발행된다.
@Slf4j
@Configuration
public class DataStreamConfig {
@Bean
public Supplier<String> basicProducer() {
return () -> "Hello";
}
@Bean
public Function<String, String> uppercase() {
return value -> value.toUpperCase();
}
@Bean
pubilc Consumer<String> basicConsumer() {
return message -> log.info("message = {}", message);
}
}
불규칙하게 발행되는 경우 StreamBridge를 활용 할수 있다.
private void processChangeHistory(long elapsedTimeMills, String className, String methodName, Sample content) {
SampleDTO sampleDTO = new SampleDTO();
sampleDTO.setCategory("change");
sampleDTO.setContent(content);
sampleDTO.setClassName(className);
sampleDTO.setMethodName(methodName);
sampleDTO.setElapsedMills(elapsedTimeMills);
streamBridge.send("historyDb", sampleDTO);
}
Spring Cloud Stream v3.x에서 org.springframework.cloud.stream.annotation 패키지에 포함된 대부분의 어노테이션이 Depreaceted 되었다.
따라서 v3.x이상에서는 함수형 프로그래밍 방식으로 작성 및 설정 해야 한다.
@EnableBinding@StreamListener@Input@Output@StreamMessageConverter
대표적으로 RabbitMQ Binder 및 Kafka Binder를 지원하며 그외에도 다양한 바인더를 지원한다.
<!-- Spring Cloud Stream RabbitMQ Binder -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-stream-binder-rabbit</artifactId>
<version>3.2.4</version>
</dependency>
<!-- Spring Cloud Stream Kafka Binder -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-stream-binder-kafka</artifactId>
<version>3.2.4</version>
</dependency>
spring:
rabbitmq:
host: 192.168.100.50
port: 5672
username: guest
password: guest
virtual-host: egov
spring:
cloud:
stream:
kafka:
binder:
autoCreateTopics: false
autoAddPartitions: false
zkNodes: 192.168.100.50
brokers: 192.168.100.50
단일 바인딩의 경우 다음과 같이 간단하게 설정 가능 하다.
spring:
cloud:
stream:
bindings:
output:
destination: sample-topic
input:
destination: sample-topic
다음 네이밍 컨벤션을 반드시 따라야 한다.
input : {functionName} + -in- + {index}
output : {functionName} + -out- + {index}
spring:
cloud:
stream:
bindings:
basicProducer-out-0:
destination: test-topic
binder: kafka
basicConsumer-in-0:
destination: test-topic
binder: rabbit
function:
definition: basicProducer;basicConsumer;
Swagger는 Restful 서비스 사용시 구현된 서비스에 대한 문서화를 지원하는 도구이다.
Restful 서비스를 구현한 경우 해당 API서버가 어떤 스펙을 가지고 있고 어떤 데이터를 주고 받는지에 대한 문서작업은 꼭 필요하다.
하지만 이런 문서작업은 상당한 시간을 사용하여 작성하여야 하고 API서버의 스펙이 변경되면 문서도 수정해 주어야 하기 때문에 관리가 여간 어려운게 아니다.
따라서 API 서버의 서비스를 작성하는것외에 문서의 작성과 유지보수를 위해 많은 시간과 비용이 발생한다.
Swagger는 이러한 Restful서비스의 문서작성과 유지보수에 대한 효율성을 높일수 있다.

그룹별로 정리되기 위해서는 URL경로가 업무별로 구분가능하고 정리되어 있어야 한다.
해당 서비스에 대해 기본적인 정보를 안내할수 있다.




<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.9.2</version>
</dependency>
<context:component-scan base-package="egovframework">
<context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
<context:exclude-filter type="annotation" expression="org.springframework.stereotype.Service"/>
<context:exclude-filter type="annotation" expression="org.springframework.stereotype.Repository"/>
</context:component-scan>
<bean id="swagger2Config"
class="springfox.documentation.swagger2.configuration.Swagger2DocumentationConfiguration"></bean>
<mvc:resources location="classpath:/META-INF/resources/"
mapping="swagger-ui.html"></mvc:resources>
<mvc:resources
location="classpath:/META-INF/resources/webjars/"
mapping="/webjars/**"></mvc:resources>
<servlet>
<servlet-name>swagger</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/config/egovframework/springmvc/swagger-servlet.xml</param-value>
</init-param>
<load-on-startup>2</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>swagger</servlet-name>
<url-pattern>/swagger-ui.html</url-pattern>
<url-pattern>/webjars/**</url-pattern>
<url-pattern>/</url-pattern>
</servlet-mapping>
@EnableSwagger2 어노테이션을 반드시 추가하여야 한다.
@Configuration
@EnableSwagger2
@EnableWebMvc
public class SwaggerConfig {
@Bean
public Docket newsApiAll() {
return new Docket(DocumentationType.SWAGGER_2)
.groupName("00. All Device API REST Service")
.apiInfo(apiInfo())
.select()
.paths(PathSelectors.any())
.build();
}
@Bean
public Docket newsApiAccelerator() {
return new Docket(DocumentationType.SWAGGER_2)
.groupName("01. Accelerator Guide Program Service")
.apiInfo(apiInfo())
.select()
.paths(regex("/acl.*"))
.build();
}
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("표준프레임워크 DeviceAPI 연계서비스 (Hybrid App)")
.description("표준프레임워크 하이브리드앱 실행환경 - iOS / Android 하이브리드앱 Rest 서비스")
.termsOfServiceUrl("https://www.egovframe.go.kr/wiki/doku.php?id=egovframework:hyb:gate_page")
.license("Apache License Version 2.0")
.licenseUrl("https://www.egovframe.go.kr")
.version("3.10")
.build();
}
}
배치처리 서비스는 일괄처리 업무 구현에 필요한 기능을 제공한다.
전자정부 표준프레임워크에서 대용량 데이터 처리 지원을 위해 작업 수행, 결과 관리, 스케줄링 관리 기능을 제공한다.
배치 실행환경은 대용량 데이터 처리를 위한 기반 환경을 제공함으로써 배치 실행에 필요한 핵심 기능을 제공한다.
전자정부 표준프레임워크 실행환경에 추가된 배치 실행환경은 3-Tier(Run, Job, Application Tier)로 구성되며, 대용량 데이터 처리를 위한 기반 환경을 제공한다.

Run Tier는 배치 응용 프로그램의 실행을 담당한다. 실행 방식에 따라 Scheduler, Http/Web service, CommandLine으로 나눌 수 있다.
✔ Spring 배치에서는 Scheduler 실행을 위해 Quartz나 Cron을 이용하도록 권고하고 있다.
Run Tier에서의 동작 순서는 다음과 같다:
Job Tier는 전체적인 Job 수행을 책임지며, 각 Step을 지정된 상태와 정책에 따라 순차적으로 수행한다.
Job Tier에서의 동작 순서는 다음과 같다:
Application Tier는 Job과 Step을 수행하는 데 필요한 컴포넌트로 구성된다.
Application Tier에서의 동작 순서는 다음과 같다:
전자정부 표준프레임워크 실행환경에 포함된 대용량 데이터 처리 계층은 Job 구조를 정의하는 Batch Core, Job 실행을 지원하는 Batch Support, 다양한 실행환경을 지원하는 Batch Execution으로 구성되어 있다. 배치 실행환경의 기술 요소와 기능은 다음 그림과 같다.

SQLite ↑
SQLite를 사용한 경량화된 Repository 사용법을 설명한다.
Logback logging ↑
SQLite를 사용한 경량화된 로깅 처리의 기본 사용법을 설명한다.
배치 처리시 경량화된 Repository를 사용을 위한 SQLite 처리를 지원한다.
sqlite 라이브러리 사용을 위해 dependency를 추가 한다.
<dependency>
<groupId>org.xerial</groupId>
<artifactId>sqlite-jdbc</artifactId>
<version>x.x.x</version>
</dependency>
SQLite 사용을 위해 데이터베이스 설정을 하고 repository 생성을 위한 기초데이터를 설정 한다.
<!-- SQLite database 설정 -->
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="org.sqlite.JDBC" />
<property name="url" value="jdbc:sqlite:repository.sqlite" />
<property name="username" value="" />
<property name="password" value="" />
</bean>
<!-- SQLite 기초데이터 설정 -->
<jdbc:initialize-database data-source="dataSource">
<jdbc:script location="org/springframework/batch/core/schema-drop-sqlite.sql" />
<jdbc:script location="org/springframework/batch/core/schema-sqlite.sql" />
</jdbc:initialize-database>
배치 처리시 로깅 처리를 위해 log4j2를 지원하고 있지만 경량화된 로깅 처리를 위해 Logback 로깅 처리를 지원한다
log4j, commons-logging 관련 라이브러리를 exclusion 처리하고, Logback 라이브러리를 등록한다.
<!-- log4j 관련 exclusion -->
<dependency>
<groupId>egovframework.rte</groupId>
<artifactId>egovframework.rte.bat.core</artifactId>
<version>${egovframework.rte.version}</version>
<exclusions>
<exclusion>
<artifactId>log4j-core</artifactId>
<groupId>org.apache.logging.log4j</groupId>
</exclusion>
<exclusion>
<artifactId>log4j-slf4j-impl</artifactId>
<groupId>org.apache.logging.log4j</groupId>
</exclusion>
<exclusion>
<artifactId>log4j-over-slf4j</artifactId>
<groupId>org.slf4j</groupId>
</exclusion>
<exclusion>
<artifactId>commons-logging</artifactId>
<groupId>commons-logging</groupId>
</exclusion>
</exclusions>
</dependency>
<!-- commons-logging 관련 exclusion -->
<exclusion>
<artifactId>commons-logging</artifactId>
<groupId>commons-logging</groupId>
</exclusion>
<!-- logback 관련 라이브러리 등록 -->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-core</artifactId>
<version>1.1.7</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.1.7</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>jcl-over-slf4j</artifactId>
<version>1.7.21</version>
</dependency>
logback 사용을 위해 logback.xml를 설정이 선행 되어야 한다. 설정관련 자세한 사항을 아래 링크 참고
<!-- 설정 예시 -->
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>[logback]%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<logger name="java.sql" level="DEBUG" />
<logger name="egovframework" level="DEBUG" />
<logger name="jdbc.sqltiming" level="DEBUG" />
<logger name="org.springframework" level="DEBUG" />
<root level="DEBUG">
<appender-ref ref="STDOUT" />
</root>
</configuration>
Job은 배치작업 전체의 중심 개념으로 배치작업 자체를 의미한다. Job은 실제 프로세스가 진행되는 Step 들을 최상단에서 포함하고 있으며, Job의 실행은 배치작업 전체의 실행을 의미한다.
아래 그림을 보면, ‘EndOfDay’라는 Job이 있고 ‘2012/10/01’이라는 JobParameter를 통해 JobInstance가 생성되었다. 그리고 ‘EndOfDay’ Job의 첫번째 시도를 의미하는 JobExecution이 생성되는 것을 볼 수 있다.

Job 인터페이스의 기본적인 구현은 SimpleJob 클래스로 스프링 배치에서 제공된다. SimpleJob 클래스는 모든 Job에서 유용하게 사용할 수 있는 표준 기능을 갖고있다. Job은 아래와 같이 <job> 태그를 사용하여 설정할 수 있다.
<job id="footballJob">
<step id="playerload" next="gameLoad"/>
<step id="gameLoad" next="playerSummarization"/>
<step id="playerSummarization"/>
</job>
JobInstance는 논리적 Job 실행의 개념으로 JobInstance = Job + JobParameters로 표현할 수 있다. 다시 말해, JobInstance는 동일한 Job이 각기 다른 JobParameter를 통해 실행 된 Job의 실행 단위이다. (Job과 JobParameters가 같으면 동일한 JobInstance이다

위의 그림을 예로 설명하면 매일 한번씩 실행되는 ‘EndOfDay’라는 Job이 있다고 가정한다. ‘EndOfDay’라는 Job은 하나지만 매일 실행되는 각각의 ‘EndOfDay’ Job은 구별되어야 한다. ‘2012/10/01’에 실행 된 ‘EndOfDay’ Job과 ‘2012/10/02’에 실행 된 ‘EndOfDay’ Job은 같은 Job이지만 JobInstance가 다르다. 이런 특성을 이용해 JobInstance는 Job의 Restart에 이용할 수 있다. JobInstance를 Restart하는 것은 해당 JobInstance의 정보(Execution Context)를 재사용하는 것이므로 새로운 JobInstance를 생성하지 않는다.(새로운 JobExecution이 생성된다.)
아래 표는 ‘EndOfDay’라는 하나의 Job이 JobParameter로 구별되어 각기 다른 JobInstance를 생성할 수 있음을 보여준다.
| JobInstance ID | Job Name | JobParameters |
|---|---|---|
| 1 | EndOfDay | 2012/10/01 |
| 2 | EndOfDay | 2012/10/02 |
JobParameters는 하나의 Job에 존재할 수 있는 여러개의 JobInstance를 구별하기 위한 Parameter 집합이며, Job을 시작하는데 사용하는 Parameter 집합이다. 또한 Job이 실행되는 동안에 Job을 식별하거나 Job에서 참조하는 데이터로 사용된다. 위의 그림(JobInstance 부분)으로 예를들면 ‘EndOfDay’ Job으로 2개의 JobInstance가 생성됐다. 이 2개의 JobInstance는 각기 다른 JobParameters(‘2012/10/01’, ‘2012/10/02’)를 통해 생성된 것이다. 아래 표에서 JobInstance는 각각의 JobParameters를 갖고 있음을 볼 수 있다.
| JobInstance ID | JobParameters | Job Name |
|---|---|---|
| 1 | 2012/10/01 | EndOfDay |
| 2 | 2012/10/02 | EndOfDay |
public class JobParameter implements Serializable {
private final Object parameter;
private final ParameterType parameterType;
}
public enum ParameterType {
STRING, DATE, LONG, DOUBLE;
}
public class JobParameters implements Serializable {
private final Map<String,JobParameter> parameters;
public JobParameters() {
this.parameters = new LinkedHashMap<String, JobParameter>();
}
public JobParameters(Map<String,JobParameter> parameters) {
this.parameters = new LinkedHashMap<String,JobParameter>(parameters);
}
protected JobParameters getUniqueJobParameters() {
return new JobParametersBuilder(super.getUniqueJobParameters())
.addString("inputFile","data/iosample/input/delimited.csv")
.addString("outputFile","file:./target/test-outputs/delimitedOutput.csv").toJobParameters();
}
new DefaultJobParametersConverter()
.getJobParameters(PropertiesConverter
.stringToProperties("run.id(long)=1,parameter=true,run.date=20121001"));
<bean id="itemReader" class="org.springframework.batch.item.file.FlatFileItemReader" >
<property name="resource" value="#{jobParameters[inputFile]}" />
(중략...)
</bean>
<bean id="itemReader" class="org.springframework.batch.item.file.FlatFileItemWriter" >
<property name="resource" value="#{jobParameters[outputFile]}" />
(중략...)
</bean>
JobExecution은 한번의 Job 시도를 의미하는 기술적인 개념이다. JobExecution은 ‘FAILED’ 또는 ‘COMPLETED’로 Job의 시도 결과를 나타낸다.
이외에, JobExecution은 주로 Job이 실행 중에 어떤 일이 일어났는지에 대한 속성들을 저장하는 저장 메커니즘 역할을 한다. (JobExecution 속성 자세히 보기)
아래의 그림을 예로들면, ‘EndOfDay’ Job은 2개의 JobInstance를 갖고 3개의 JobExecution이 존재하는 것을 볼 수 있다. JobInstance가 매일 한번 실행되는 Job을 구분하는 논리적인 개념이라면, JobExecution은 3번의 Job 시도 자체를 의미한다.
‘2012/10/02’라는 JobParameter로 실행된 ‘EndOfDay’ Job은 ID가 2인 JobInstance를 생성하게 된다. 첫번째 Job의 시도는 FAILED로 끝나게 됐고 두번째 시도는 COMPLETED로 완료하게 된다.
즉, ‘2012/10/02’에 ‘EndOfDay’ Job은 총 2번의 Job 시도로 2개의 JobExecution이 생성됐다.
✔ Status가 COMPLETED 인 JobExecution을 가진 JobInstance는 restart를 할 수 없다.(해당 JobInstance는 정상적으로 배치작업을 완료)

아래의 표를 보면 ‘EndOfDay’ Job이 각각 다른 ‘2012/10/01’, ‘2012/10/02’ JobParameter로 두 번 실행결과 JobInstance는 2개가 생성 됐고, Job의 3번 시도에 따라 3개의 JobExecution이 생성된 것을 볼 수 있다. 여기서 중요한 점은 JobExecution은 매 시도마다 새로 생성되지만 JobInstance가 같은 JobExecution은 동일한 JobParameter로 시도 됐다는 점이다.
| JobExecution ID | JobInstance ID | Start Time | End Time | Status |
|---|---|---|---|---|
| 1 | 1 | 2012.10.01.12:00 | 2012.10.01.12:10 | COMPLETED |
| 2 | 2 | 2012.10.02.12:00 | 2012.10.02.12:10 | FAILED |
| 3 | 2 | 2012.10.02.12:30 | 2012.10.02.12:40 | COMPLETED |
일반적인 스프링 프로젝트에서 Job은 XML 설정 파일을 통해 표현되며, 의존성을 맺게 되고 이 설정 파일은 “Job 설정”이라 한다.
<job id="footballJob" job-repository="specialRepository">
<step id="playerload" parent="s1" next="gameLoad"/>
<step id="gameLoad" parent="s3" next="playerSummarization"/>
<step id="playerSummarization" parent="s3"/>
</job>
‘parent’ 속성은 Job을 상속하여 유사한 설정의 Job이 여러 개일 경우 유용하게 사용할 수 있다. Java에서 클래스 상속과 유사하게 자식 Job은 부모 Job의 속성들과 자신의 속성들을 결합한다. 또한 부모 Job의 속성을 오버라이드 하여 사용할 수도 있다.
Java의 Abstract 클래스와 동일한 개념으로 때로는 완전한 Job을 구성하지 않는 부모 Job의 정의가 필요할 때가 있다. ‘abstract’ 속성은 Job 설정이 추상레벨인지 여부를 지정한다.
아래 예제에서 “baseJob”은 abstract 선언되었으며 자식 Job인 “job1”에서는 Job 설정의 필수요소인 ‘<step>‘을 정의해야 하며 ‘Job1’은 “baseJob”의 “listenerOne”도 상속받아 설정된다.
<job id="baseJob" abstract="true">
<listeners>
<listener ref="listenerOne"/>
<listeners>
</job>
<job id="job1" parent="baseJob">
<step id="step1" parent="standaloneStep"/>
<listeners merge="true">
<listener ref="listenerTwo"/>
<listeners>
</job>
Job은 완료되지 않은 JobInstance를 재시작할 수 있다. Job 설정 시, 해당 Job에 만들어진 JobInstance들의 재시작 가능 여부를 ‘restartable’ 속성을 통해 설정할 수 있다
<job id="footballJob" restartable="false">
...
</job>
변수 선언 후 Listeners를 통해서 모든 Job에서 사용자 정의 변수를 사용할 수 있도록 EgovJobVariableListener를 통해서 지원한다.
변수 선언 후 Job Listeners를 통해서 모든 Job에서 사용자 정의 변수를 사용할 수 있도록 EgovJobVariableListener를 통해서 지원한다.
사용자가 변수를 정의하여 여러 job에서 해당 변수를 공유하여 사용 가능한 기능으로 이루어져있다.

배치실행환경에서 제공하는 EgovJobVariableListener 사용하여 사용자 정의 변수를 설정한다.
<bean id="egovJobVariableListener" class="egovframework.rte.bat.support.EgovJobVariableListener">
<property name="pros">
<props>
<prop key="JobVariableKey1">JobVariableValue1</prop>
<prop key="JobVariableKey2">JobVariableValue2</prop>
<prop key="JobVariableKey3">JobVariableValue3</prop>
</props>
</property>
</bean>
job 설정시 listener를 사용하여 공유변수 서비스를 설정한다.
<job id="delimitedToDelimitedJob-JobVariable" parent="eGovBaseJob" xmlns="http://www.springframework.org/schema/batch">
<listeners>
<listener ref="egovJobVariableListener" />
</listeners>
<step id="step1">
<tasklet ref="taskletJob" />
</step>
</job>
setter 방식으로 공유변수 사용시 아래와 같이 응용하여 설정한다.
<bean id="taskletJob" class="egovframework.example.bat.step.TaskletJob" scope="step">
<property name="jobVariable" value="#{jobExecutionContext[JobVariableKey1]}" />
</bean>
public class TaskletJob implements Tasklet, InitializingBean {
private String jobVariable;
@Value("#{jobExecutionContext[JobVariableKey2]}")
private String vJobVariable;
public String getJobVariable() {
return jobVariable;
}
public void setJobVariable(String jobVariable) {
this.jobVariable = jobVariable;
}
@Override
public RepeatStatus execute(StepContribution contribution,
ChunkContext chunkContext) throws Exception {
//Tasklelt 선언시 setter의해 선언된 Job Variable : jobVariable
//annotation @Value 통해 선언된 Job Variable : vJobVariable
//direct 접근을 통한 Job Variable 사용 : chunkContext.getStepContext().getJobExecutionContext().get("JobVariableKey3")
return RepeatStatus.FINISHED;
}
}
Step은 Job 내부에 구성되어 실제 배치작업 수행을 위해 작업을 정의하고 제어한다. 즉, Step에서는 입력 자원을 설정하고 어떤 방법으로 어떤 과정을 통해 처리할지 그리고 어떻게 출력 자원을 만들 것인지에 대한 모든 설정을 포함한다.
Step은 Job의 독립적이고 순차적 단계를 캡슐화하는 도메인 객체다. 그러므로 모든 Job은 적어도 하나 이상의 Step으로 구성되며 Step에 실제 배치작업을 처리하고 제어하기 위해 필요한 모든 정보가 포함된다. 여러개의 Step 중 하나의 Step은 순차적으로 실행되는 과정 중 하나의 흐름으로 생각할 수 있다. Step에는 JobExecution에 대응되는 StepExecution이 있다.

스프링 빈 선언시 Bean Scope 기본 전략을 singleton를 사용하고 있지만, 스프링 배치에서는 step에서 Bean Scope에 대해 job, step 설정이 가능하다.
scope step : 하나의 빈 정의에 대해 step 안에서 lifecycle이 유효하다.
<bean id="..." class="..." scope="step">
scope job : 하나의 빈 정의에 대해 job 안에서 lifecycle이 유효하다.
<bean id="..." class="..." scope="job">
Chunk 기반 처리는 스프링 배치에서 가장 일반적으로 사용하는 Step 유형이다. Chunk 기반 처리는 data를 한번에 하나씩 읽고, 트랜잭션 범위 내에서 ‘Chunk’를 만든 후 한번에 쓰는 방식이다. 즉, 하나의 item이 ItemReader를 통해 읽히고, Chunk 단위로 묶인 item들이 한번에 ItemWriter로 전달 되어 쓰이게 된다.
Chunk 단위로 Item 읽기 → 처리/변환 → 쓰기의 단계를 거치는 Chunk 기반 처리 매커니즘은 다음과 같다

아래 코드는 위의 그림과 같은 개념의 코드이다.
List items = new Arraylist();
for(int i = 0; i < commitInterval; i++){
Object item = itemReader.read()
Object processedItem = itemProcessor.process(item);
items.add(processedItem);
}
itemWriter.write(items);
배치작업을 적용한 업무 환경에 따라 ItemReader와 ItemWriter를 활용한 구조가 맞지 않는 경우도 있을 것이다. 예를들어 단순히 DB의 프로시저 호출만으로 끝나는 배치처리가 있다면 단순히 메소드 하나로 기능을 구현하고 싶어질 것이다. 이런 경우를 위해 스프링 배치에서는 TaskletStep을 제공한다. Tasklet은 RepeatStatus.FINISHED를 반환하거나 에러가 발생하기 전까지 계속 실행하는 execute() 하나의 메소드를 갖는 간단한 인터페이스로 저장 프로시저, 스크립트, 또는 간단한 SQL 업데이트 문을 호출 할 수 있다.
TaskletStep을 구성하기 위해서는 <tasklet> 태그의 ‘ref’속성을 통해 Tasklet 객체를 참조해야한다. <chunk> 태그는 <tasklet> 내에서 사용되지 않는다.(<Chunk> 태그는 Chunk-Oriented Processing에서 사용된다.)
<step id="step1">
<tasklet ref="myTasklet"/>
</step>
Tasklet 인터페이스를 구현한 SystemCommandTasklet 클래스를 이용해서 “echo hello"라는 명령어를 5초 동안의 timeout시간을 두고 실행시키는 설정의 예
<bean id="myTasklet">
<property name="tasklet">
<bean class="org.springframework.batch.sample.tasklet.SystemCommandTasklet">
<property name="command" value="echo hello" />
<property name="timeout" value="5000" />
</bean>
</property>
</bean>
Job의 JobExecution과 대응되는 단위로 Step 또한 StepExecution을 갖고 있다. JobExecution과 마찬가지로 StepExecution은 Step을 수행하기 위한 단 한번의 Step 시도를 의미하며 매번 시도될 떄마다 생성된다. 또한, StepExecution은 주로 Step이 실행 중에 어떤 일이 일어났는지에 대한 속성들을 저장하는 저장 메커니즘 역할을 하며 commit count, rollback count, start time, end time 등의 Step 상태정보를 저장한다. (StepExecution 속성 자세히 보기)
아래의 그림에서 ‘Step1’, ‘Step2’ 2개의 Step을 갖는 ‘EndOfDay’ Job이 두번 실행되었다고 가정하자.(두번 시도 결과 JobExecution은 2개 생성) ‘EndOfDay’ Job을 시도할 때마다 ‘Step1’, ‘Step2’도 시도 되기때문에 StepExecution은 2개 씩 생성된다. 그래서 총 4개의 StepExecution이 생성된 것을 볼 수 있다.
✔ StepExecution 4번이 FAILED로 종료 됐으므로 StepExecution 3번, 4번을 포함한 JobExecution 2번은 FAILED로 종료한다.(Step이 모두 정상적으로 완료해야 Step으로 구성된 Job이 정상적으로 완료된다.)

위의 그림을 정리해보면 아래와 같다.
StepExecution ID Step Name JobExecution ID Status
| StepExecution ID | Step Name | JobExecution ID | Status |
|---|---|---|---|
| 1 | Step1 | 1 | COMPLETED |
| 2 | Step2 | 1 | COMPLETED |
| 3 | Step1 | 2 | COMPLETED |
| 4 | Step2 | 2 | FAILED |
Step 구성은 개발자에 따라 간단하거나 아주 복잡하게 구성할 수 있다. 다만 구성을 쉽게하기 위해 스프링 배치 네임스페이스를 사용할 수 있다.
<job id="sampleJob" job-repository="jobRepository">
<step id="step1">
<tasklet transaction-manager="transactionManager">
<chunk reader="itemReader" writer="itemWriter" commit-interval="10"/>
<tasklet>
</step>
</job>
✔ ItemProcessor 속성은 옵션이며 ItemProcessor가 없는 경우 reader에서 writer로 직접 전달된다.
‘parent’ 속성은 Step을 상속하여 유사한 설정의 Step이 여러 개일 경우 유용하게 사용할 수 있다. Java에서 클래스 상속과 유사하게 자식 Step은 부모 Step의 속성들과 자신의 속성들을 결합한다. 또한 부모 Step의 속성을 오버라이드 하여 사용할 수도 있다. 아래 예제에서 “parentStep"을 상속받은 “concreteStep1"은 ‘itemReader’, ‘itemProcessor’, ‘itemWriter’, startLimit=5, allowStartIfComplete=true로 설정되며, commit-interval은 5로 오버라이드하여 설정된다.
<step id="parentStep">
<tasklet allow-start-if-complete="true">
<chunk reader="itemReader" writer="itemWriter" commit-interval="10"/>
</tasklet>
</step>
<step id="concreteStep1" parent="parentStep">
<tasklet start-limit="5">
<chunk processor="itemProcessor" commit-interval="5"/>
</tasklet>
</step>
Java의 Abstract 클래스와 동일한 개념으로 때로는 완전한 Step을 구성하지 않는 부모 Step의 정의가 필요할 때가 있다. ‘abstract’ 속성은 Step 설정이 추상레벨인지 여부를 지정한다. 아래 예제에서 “abstractParentStep"은 abstract 선언되었으며 자식 Step인 “concreteStep2"에서는 Step 설정의 필수요소인 ‘itemReader’, ‘itemWriter’, ‘commitInterval’를 정의해야 한다.
<step id="abstractParentStep" abstract="true">
<tasklet>
<chunk commit-interval="10"/>
</tasklet>
</step>
<step id="concreteStep2" parent="abstractParentStep">
<tasklet>
<chunk reader="itemReader" writer="itemWriter"/>
</tasklet>
</step>
부모 Step을 상속받아 자식 Step에서 동일한 속성을 정의하는 경우 기본적으로 오버라이딩된다. 그러나 ‘merge’ 속성을 이용해 자녀 Step이 부모 Step에 의해 정의 된 리스너에 추가 리스너를 추가 할 수 있다.(<listeners>를 포함한 list 속성에서 사용 가능) 아래 예제에서 “concreteStep3” Step은 “listenerTwo”와 “listenerOne”를 모두 사용할 수 있다
<step id="listenersParentStep" abstract="true">
<listeners>
<listener ref="listenerOne"/>
<listeners>
</step>
<step id="concreteStep3" parent="listenersParentStep">
<tasklet>
<chunk reader="itemReader" writer="itemWriter" commit-interval="5"/>
</tasklet>
<listeners merge="true">
<listener ref="listenerTwo"/>
<listeners>
</step>
각 Step 실행 횟수를 설정한다. default는 SimpleStepFactoryBean 클래스에서 셋팅 되며 Integer.MAX_VALUE로 설정 되있다.
Job의 Restart 시, “COMPLETED”로 완료한 Step의 실행 여부를 설정한다. true로 설정 시, “COMPLETED”로 완료한 Step도 다시 실행되며 이전 시도의 결과를 오버라이드 한다. (false 설정 시, “COMPLETED”로 완료한 Step은 skip)
아래 예제에서 “step1” Step은 10번만 실행 가능하며 Job을 Restart 했을 시, 이전 시도와 관계없이 재실행 된다.있다
<step id="step1">
<tasklet allow-start-if-complete="true" start-limit="10">
<chunk reader="itemReader" writer="itemWriter" commit-interval="10"/>
</tasklet>
</step>
Step 흐름제어(Flow Control) 자세히 보기
변수 선언 후 Listeners를 통해서 모든 Setp에서 사용자 정의 변수를 사용할 수 있도록 EgovStepVariableListener를 통해서 지원한다.
변수 선언 후 Listeners를 통해서 모든 Setp에서 사용자 정의 변수를 사용할 수 있도록 EgovStepVariableListener를 통해서 지원한다. 사용자가 변수를 정의하여 여러 step에서 해당 변수를 공유하여 사용 가능한 기능으로 이루어져있다.

배치실행환경에서 제공하는 EgovJobVariableListener 사용하여 사용자 정의 변수를 설정한다.
<bean id="egovStepVariableListener" class="egovframework.rte.bat.support.EgovStepVariableListener">
<property name="pros">
<props>
<prop key="StepVariableKey1">StepVariableValue1</prop>
<prop key="StepVariableKey2">StepVariableValue2</prop>
<prop key="StepVariableKey3">StepVariableValue3</prop>
</props>
</property>
</bean>
step 설정시 listener를 사용하여 공유변수 서비스를 설정한다.
<job id="delimitedToDelimitedJob-StepVariable" parent="eGovBaseJob" xmlns="http://www.springframework.org/schema/batch">
<step id="step1">
<tasklet ref="taskletStep" />
<listeners>
<listener ref="egovStepVariableListener" />
</listeners>
</step>
</job>
setter 방식으로 공유변수 사용시 아래와 같이 응용하여 설정한다.
<bean id="taskletStep" class="egovframework.example.bat.step.TaskletStep" scope="step">
<property name="stepVariable" value="#{stepExecutionContext[StepVariableKey1]}" />
</bean>
public class TaskletStep implements Tasklet, InitializingBean {
private String stepVariable;
@Value("#{stepExecutionContext[StepVariableKey2]}")
private String vStepVariable;
public String getStepVariable() {
return stepVariable;
}
public void setStepVariable(String stepVariable) {
this.stepVariable = stepVariable;
}
@Override
public RepeatStatus execute(StepContribution contribution,
ChunkContext chunkContext) throws Exception {
//Tasklelt 선언시 setter의해 선언된 Step Variable : stepVariable
//annotation @Value 통해 선언된 Step Variable : vStepVariable
//direct 접근을 통한 Step Variable 사용 : chunkContext.getStepContext().getStepExecutionContext().get("StepVariableKey3")
return RepeatStatus.FINISHED;
ItemReader는 읽기 대상의 타입에 관계없이 한번에 한 항목을 읽으며 읽을 항목이 모두 소진되면 Null을 반환하는 인터페이스이다.
ItemReader는 여러 종류의 데이터 타입을 입력 받을 수 있다. 가장 일반적인 데이터 타입으로 플랫 파일, XML, 데이터베이스가 있다.
기본적인 ItemReader 인터페이스는 아래와 같다.
public interface ItemReader<T> {
T read() throws Exception, UnexpectedInputException, ParseException;
}
read() 메소드는 ItemReader의 필수적인 메소드이며 결과값으로 하나의 item을 반환하고 더이상 반환할 item이 없을 경우 null을 반환한다. item은 플랫 파일에서의 한 라인, 데이터베이스에서의 한 행, XML 파일에서의 엘리먼트를 나타낸다.
플랫파일은 2차원 데이터를 포함하는 유형의 파일이다. 스프링 배치 프레임워크에서는 플랫파일을 읽고 파싱하는 기본적인 기능을 제공하는 FlatFileItemReader 클래스를 통해 플랫파일에 대한 읽기 처리를 한다.
FlatFileItemReader는 Resource, LineMapper, FieldSetMapper, LineTokenizer에 기본적으로 의존성을 갖으며, LineTokenizer에 따라 구분자(Delimited)와 고정길이(Fixed Length) 방식으로 FlatFileItemReader를 사용할 수 있다.

| 구분 | 데이터 형태 | 설명 |
|---|---|---|
| LineMapper | 플랫파일 1 라인(String) → Object | 플랫파일 데이터에서 읽은 1 라인(String)을 Object로 변환하는 총 과정(LineTokenizer, FieldSetMapper 과정을 포함한다.) |
| LineTokenizer | String → Tokens → FieldSet | 플랫파일에서 읽은 1 라인(String)을 구분자 방식 또는 고정길이 방식으로 토크나이징 한 후 FieldSet 형태로 변환하는 과정 - DelimitedLineTokenizer (구분자) : 1 라인의 String을 구분자 기준으로 나누어 토큰화 하는 방식 - EgovEscapableDelimitedLineTokenizer : Escape 문자를 사용하여 Delimiter(구분자) 문자를 문자열에 추가할 수 있는 방식 (예: 1”,000원 → 1,000원 으로 인식) - FixedLengthTokenizer (고정길이) : 1 라인의 String을 사용자가 설정한 고정길이 기준으로 나누어 토큰화 하는 방식 |
| FieldSetMapper | FieldSet → Object | FieldSet 형태의 데이터를 원하는 Object로 변환하는 과정 |
아래 Delimited(구분자), Fixed Length(고정길이) 방식으로 설정한 FlatFileItemReader의 예시를 통해 FlatFileItemReader, LineMapper, LineTokenizer, FieldSetMapper의 의존 관계를 볼 수 있다.
| Tokenizing 방식 | 설정 |
|---|---|
| Delimited (구분자) | <bean id="itemReader" class="org.springframework.batch.item.file.FlatFileItemReader" scope="step"> <property name="resource" value="#{jobParameters[inputFile]}" /> <property name="lineMapper"> <bean class="org.springframework.batch.item.file.mapping.DefaultLineMapper"> <property name="lineTokenizer"> <bean class="org.springframework.batch.item.file.transform.DelimitedLineTokenizer"> <property name="delimiter" value=","/> <property name="names" value="name,credit" /> </bean> </property> <property name="fieldSetMapper"> <bean class="org.springframework.batch.item.file.mapping.BeanWrapperFieldSetMapper"> <property name="targetType" value="org.springframework.batch.CustomerCredit" /> </bean> </property> </bean> </property> </bean> |
| Tokenizing 방식 | 설정 |
| EscapableDelimited | <<bean id="delimitedToDelimitedJob-EscapeCharacter-delimitedItemReader" class="org.springframework.batch.item.file.FlatFileItemReader" scope="step"> <property name="resource" value="#{jobParameters[inputFile]}" /> <property name="lineMapper"> <bean class="egovframework.rte.bat.core.item.file.mapping.EgovDefaultLineMapper"> <property name="lineTokenizer"> <bean class="egovframework.rte.bat.core.item.file.transform.EgovEscapableDelimitedLineTokenizer"> <property name="delimiter" value="," /> <property name="escape" value="true" /> <property name="quoteCharacter" value=""" /> </bean> </property> <property name="objectMapper"> <bean class="egovframework.rte.bat.core.item.file.mapping.EgovObjectMapper"> <property name="type" value="egovframework.example.bat.domain.trade.CustomerCreditMore" /> <property name="names" value="id,name,credit,serial,tax,amount,createDate,changeDate" /> </bean> </property> </bean> </property> </bean> |
| Fixed Length (고정길이) | <bean id="itemReader" class="org.springframework.batch.item.file.FlatFileItemReader" scope="step"> <property name="resource" value="#{jobParameters[inputFile]}" /> <property name="lineMapper"> <bean class="org.springframework.batch.item.file.mapping.DefaultLineMapper"> <property name="lineTokenizer"> <bean class="org.springframework.batch.item.file.transform.FixedLengthTokenizer"> <property name="columns" value="1-9,10-11" /> <property name="names" value="name,credit" /> </bean> </property> <property name="fieldSetMapper"> <bean class="org.springframework.batch.item.file.mapping.BeanWrapperFieldSetMapper"> <property name="targetType" value="org.springframework.batch.CustomerCredit" /> </bean> </property> </bean> </property> </bean> |
LineTokenizer와 FieldSetMapper에 아래와 같은 항목을 설정해야한다.
| 설정항목 | 내용 |
|---|---|
| targetType | VO 클래스를 나타낸다. |
| names | VO 클래스의 필드를 나타낸다. |
✔ 사용하는 LineTokenizer에 따라 설정항목이 다르므로 주의하여 설정해야 한다.
| LineTokenizer | 설정항목 | 설명 | 설정 예 |
|---|---|---|---|
| DelimitedLineTokenizer | delimiter | 토크나이징 할 때 기준이 되는 구분자 설정 | , |
| EgovEscapableDelimitedLineTokenizer | delimiter | 토크나이징 할 때 기준이 되는 구분자 설정 | , |
| escape | Escape 문자 사용 설정 | true 또는 false | |
| quoteCharacter | 사용 할 Escape 문자 설정 | 쌍따움표(”, ") XML escape문자로 인하여 쌍따움표는 " 표시 | |
| FixedLengthTokenizer | columns | 토크나이징 할 때 기준이 되는 고정길이 설정 | 1-9,10-11 |
스프링 배치는 XML 레코드를 읽고 자바 객체로 매핑하는 작업에 대해 트랜잭션 인프라스트럭쳐를 제공한다. 스프링 배치에서 XML 입력과 출력이 어떻게 작동되는지 더 살펴보면 첫째로 파일 읽기 및 쓰기에 따라 차이가 있지만 스프링 배치 XML 처리 과정은 공통화 돼있다. XML 처리 과정에서 토크나이징이 필요한 레코드(FieldSets) 라인 대신 개별 레코드와 대응되는 ‘fragments’의 콜렉션으로 가정하고 있다.

위의 시나리오에서 ’trade’ 태그는 ‘루트 엘리먼트’로 정의 되었다. ‘<trade>‘와 ‘</trade>’ 사이의 모든 내용은 하나의 fragment로 여겨진다. 스프링 배치는 fragment를 객체로 바인드 하는데 Object/XML Mapping (OXM)을 사용한다. 하지만 스프링 배치는 특정 XML 바인딩 기술에 묶여있지 않다. 대표적인 사용방법은 가장 대중적인 OXM 기술에 대한 일관된 추상화를 제공하는 스프링 OXM에 위임하는 방법이다. 스프링 OXM에 대한 의존성은 선택적이며 만일 필요하다면 스프링 배치에서 특정 인터페이스를 구현하도록 선택할 수 있다. XML 지원 관련 기술 관계는 아래 그림과 같다.

StaxEventItemReader 설정은 XML 입력 스트림에서 레코드의 처리를 위한 전형적인 설정을 제공한다. 먼저, StaxEventItemReader가 처리할 수 있는 XML 레코드 집합을 검토해보자.
<?xml version="1.0" encoding="UTF-8"?>
<records>
<trade xmlns="http://springframework.org/batch/sample/io/oxm/domain">
<isin>XYZ0001</isin>
<quantity>5</quantity>
<price>11.39</price>
<customer>Customer1</customer>
</trade>
<trade xmlns="http://springframework.org/batch/sample/io/oxm/domain">
<isin>XYZ0002</isin>
<quantity>2</quantity>
<price>72.99</price>
<customer>Customer2c</customer>
</trade>
<trade xmlns="http://springframework.org/batch/sample/io/oxm/domain">
<isin>XYZ0003</isin>
<quantity>9</quantity>
<price>99.99</price>
<customer>Customer3</customer>
</trade>
</records>
XML 레코드를 처리하기 위해서는 다음 사항이 필요하다.
<bean id="itemReader" class="org.springframework.batch.item.xml.StaxEventItemReader">
<property name="fragmentRootElementName" value="trade" />
<property name="resource" value="data/iosample/input/input.xml" />
<property name="unmarshaller" ref="tradeMarshaller" />
</bean>
아래 예제에서는 XStreamMarshaller를 사용한다. XStreamMarshaller는 fragment 명과 객체 타입을 바인드 해주기 위해 사용하는 별칭을 키와 값을 포함하는 맵으로 건내주도록 했다. 그 다음 FieldSet과 비슷하게 맵에 엘리먼트 이름과 타입이 키/값 쌍으로 들어가게 된다. 다음처럼 설정 파일에서 필요한 별칭을 기술하는데 스프링 설정 유틸리티를 사용할 수 있다.
<bean id="tradeMarshaller" class="org.springframework.oxm.xstream.XStreamMarshaller">
<property name="aliases">
<util:map id="aliases">
<entry key="trade" value="org.springframework.batch.sample.domain.Trade" />
<entry key="price" value="java.math.BigDecimal" />
<entry key="name" value="java.lang.String" />
</util:map>
</property>
</bean>
입력 리더는 (기본적으로 태그 이름의 일치에 의해서) 새로운 프레그먼트가 시작하는 것을 인식할 때까지 XML 자원을 읽어 들인다. 리더는 프레그먼트에서 독립적으로 작동하는 XML 문서를 생성하고, XML을 자바 객체로 매핑하기 위해 deserializer에게 이 문서를 전달한다. 정리해보면,
StaxEventItemReader xmlStaxEventItemReader = new StaxEventItemReader()
Resource resource = new ByteArrayResource(xmlResource.getBytes())
Map aliases = new HashMap();
aliases.put("trade","org.springframework.batch.sample.domain.Trade");
aliases.put("price","java.math.BigDecimal");
aliases.put("customer","java.lang.String");
Marshaller marshaller = new XStreamMarshaller();
marshaller.setAliases(aliases);
xmlStaxEventItemReader.setUnmarshaller(marshaller);
xmlStaxEventItemReader.setResource(resource);
xmlStaxEventItemReader.setFragmentRootElementName("trade");
xmlStaxEventItemReader.open(new ExecutionContext());
boolean hasNext = true
CustomerCredit credit = null;
while (hasNext) {
credit = xmlStaxEventItemReader.read();
if (credit == null) {
hasNext = false;
}
else {
System.out.println(credit);
}
}
대부분의 엔터프라이즈 애플리케이션처럼 데이터베이스는 배치 저장 메카니즘의 중심이 된다. 그러나 배치는 다른 애플리케이션 스타일과는 다르다. 만일 SQL 문이 백만 행을 반환하는 경우에 결과 집합은 모든 행을 읽을 때까지 메모리에 모든 결과를 보유한다. 스프링 배치는 이 문제를 해결하기 위해 Cursor와 Paging 데이터베이스 ItemReader를 제공한다.
커서 방식을 이용한 데이터베이스 접근 방식은 가장 기본적인 방식이다. 왜냐하면 ‘streaming’ 관계형 데이터의 문제에 대한 해결책이기 때문이다. Java의 ResultSet 클래스는 커서를 다루는 객체 지향 메커니즘의 필수적인 클래스이다. ResultSet은 데이터의 현재 행에 커서를 유지하며 다음 데이터를 호출하면 다음 행으로 커서를 이동한다. 스프링 배치에서 커서는 커서를 초기화해서 열어주는 ItemReader에 기반하며, read가 호출될 떄마다 커서를 다음 행으로 이동시키며 처리 과정 중에 사용되는 맵핑된 객체를 반환한다.
아래 그림의 예제는 커서 기반의 ItemReader의 작동을 보여준다. ‘FOO’ 테이블은 ID, NAME, BAR 세 개의 컬럼을 갖는다. SQL문을 통해 ID가 1보다 크고 7보다 작은 행의 결과를 조회한다. 커서는 ID 2에서 시작하며 read()가 호출될 떄마다 FOO 객체로 맵핑되고 커서는 다음 행으로 이동한다.

JdbcCursorItemReader는 커서 기반 기술의 JDBC를 구현한 ItemReader이다. 아래의 JdbcCursorItemReader의 설정 예시를 보면 굉장히 쉽게할 수 있음을 알 수 있다. dataSouce 속성으로 DB connection을 넣어올 수 있는 datasource를 지정하고, sql 속성에 실행할 쿼리, rowMapper 속성에 ResultSet에서 객체를 매핑하는 클래스로 RowMapper 인터페이스를 구현한 클래스가 필요하다.
<bean id="itemReader" class="org.springframework.batch.item.database.JdbcCursorItemReader">
<property name="dataSource" ref="dataSource"/>
<property name="sql" value="select ID, NAME, CREDIT from CUSTOMER"/>
<property name="rowMapper">
<bean class="egovframework.brte.sample.domain.trade.CustomerCreditRowMapper"/>
</property>
</bean>
데이터베이스 커서를 사용하는 대신 여러번 쿼리를 실행할 수 있는데 실행되는 각 쿼리는 정해진 크기인 페이지만큼의 결과를 가져오게 된다. 실행되는 각 쿼리는 시작 행 번호를 지정하고 페이지에 반환시키고자 하는 행의 수를 지정한 후 사용한다.
페이징 ItemReader의 구현체 중 하나인 JdbcPagingItemReader는 페이지를 형성하는 행을 반환하는데 사용하는 SQL 쿼리를 제공할 책임을 지고 있는 PagingQueryProvider 인터페이스가 필요하다. 데이터베이스 유형 별로 지원하는 OraclePagingQueryProvider, HsqlPagingQueryProvider, MySqlPagingQueryProvider ,SqlServerPagingQueryProvider,SybasePagingQueryProvider 등의 구현를 사용하지만 데이터베이스를 자동으로 식별해주고 적절한 PagingQueryProvider 구현체를 적용해주는데 사용하는 SqlPagingQueryProviderFactoryBean이 있다. SqlPagingQueryProviderFactoryBean는 환경 설정을 간단히 해주며 추천하는 구현체이다.
아래 예제는 위의 JdbcCursorItemReader 설정과 동일한 설정이다.
<bean id="itemReader" class="org.springframework.batch.item.database.JdbcPagingItemReader">
<property name="dataSource" ref="dataSource"/>
<property name="queryProvider">
<bean class="org.springframework.batch.item.database.support.SqlPagingQueryProviderFactoryBean">
<property name="dataSource" ref="dataSource" />
<property name="selectClause" value="select id, name, credit"/>
<property name="fromClause" value="from customer"/>
<property name="whereClause" value="where status=:status"/>
<property name="sortKey" value="id"/>
</bean>
</property>
<property name="parameterValues">
<map>
<entry key="status" value="NEW"/>
</map>
</property>
<property name="pageSize" value="1000"/>
<property name="rowMapper" ref="customerMapper"/>
</bean>
iBatis를 사용해 데이터에 접근하는 경우 페이징 ItemReader를 구현한 IbatisPagingItemReader를 사용할 수 있다. iBatis는 페이지의 행을 읽을 수 있는 직접적인 지원은 하지 않지만 여러 표준화된 변수를 이용해여 쿼리를 추가할 수 있다.
아래 설정은 위에서 설명한 JdbcCursorItemReader, JdbcCursorItemReader 설정과 같은 설정을 보여준다.
<bean id="itemReader" class="org.springframework.batch.item.database.IbatisPagingItemReader">
<property name="sqlMapClient" ref="sqlMapClient"/>
<property name="queryId" value="getPagedCustomerCredits"/>
<property name="pageSize" value="1000"/>
</bean>
위의 IbatisPagingItemReader 설정에서 사용한 queryId 속성의 “getPagedCustomerCredits”의 구성은 아래와 같다. ex) MySQL
<select id="getPagedCustomerCredits" resultMap="customerCreditResult">
select id, name, credit from customer order by id asc LIMIT #_skiprows#, #_pagesize#
</select>
스프링 배치에서 제공하는 파일 기반 관련설정을 사용할 경우, 대용량 데이터 처리 시간이 상용 배치프레임워크 대비 성능이 떨어졌다. 이 문제를 해결하기 위해서 전자정부에서는 파일 ItemReader의 요소 중 성능저하 요인인 LineMapper 부분을 개선하여 제공한다.
아래 그림을 보면 전자정부에서는 FieldSet을 사용하지 않는다. 따라서 Tokens → FieldSet으로 변환하는 과정이 없다. 전자정부에서 제공하는 EgovDefaultLineMapper, EgovLineTokenizer, EgovObjectMapper를 사용하는 경우 Tokens 상태에서 Object로 직접 맵핑된다.
| 스프링 FlatFileItemReader 구조 | 전자정부 eGovFlatFileItemReader 구조 |
|---|---|
![]() | ![]() |
| 개선사항 | 설명 |
|---|---|
| EgovDefaultLineMapper | EgovLineTokenizer와 EgovObjectMapper가 변경됨에 따라 LineMapper 총 과정을 제어하는 DefaultLineMapper를 변경하여 EgovDefaultLineMapper 제공 |
| EgovLineTokenizer | 전자정부에서는 FieldSet을 사용하지 않기때문에 FieldSet을 반환하는 LineTokenizer 인터페이스를 변경하여 EgovLineTokenizer 제공 |
| EgovAbstractLineTokenizer | LineTokenizer 인터페이스가 EgovLineTokenizer로 변경됨에 따라 토크나이징만 관여하는 추상 클래스 EgovAbstractLineTokenizer 제공 |
| EgovDelimitedLineTokenizer | 스프링에서 제공하는 DelimitedLineTokenizer의 성능을 개선한 EgovDelimitedLineTokenizer 제공 |
| EgovObjectMapper | 전자정부에서는 FieldSet을 사용하지 않고 토크나이징 된 값들을 직접 Object에 맵핑하는 EgovObjectMapper를 제공 |
아래의 XML 설정은 스프링에서 제공하는 DefaultLineMapper를 적용한 FlatFileItemReader와 전자정부에서 제공하는 EgovDefaultLineMapper를 적용한 FlatFileItemReader 설정 비교이다.
✔ 주의! EgovDefaultLineMapper 사용 시, 반드시 EgovTokenizer(EgovFixedLengthTokenizer, EgovByteLengthTokenizer, EgovDelimitedTokenizer)와 EgovObjectMapper를 사용해야 한다.
✔ 주의! EgovObjectMapper 사용 시, VO 필드 타입은 String, int, double, float, long, char, boolean, short, BigDecimal로 제한된다.
✔ 주의! 스프링의 DefaultLineMapper 사용 시, Tokenizer에서 ’names’ 속성을 설정하지만 전자정부의 EgovDefaultLineMapper 사용 시, EgovObjectMapper에서 ’names’ 속성을 설정한다.
읽어들인 문자열에서 구분자를 경계값으로 사용하여 필드를 분리한다.
| 구분 | 설정 |
|---|---|
| 스프링 FlatFileItemReader | <bean id="itemReader" class="org.springframework.batch.item.file.FlatFileItemReader" scope="step"> <property name="resource" value="#{jobParameters[inputFile]}" /> <property name="lineMapper"> <bean class="org.springframework.batch.item.file.mapping.DefaultLineMapper"> <property name="lineTokenizer"> <bean class="org.springframework.batch.item.file.transform.DelimitedLineTokenizer"> <property name="delimiter" value=","/> <property name="names" value="name,credit" /> </bean> </property> <property name="fieldSetMapper"> <bean class="org.springframework.batch.item.file.mapping.BeanWrapperFieldSetMapper"> <property name="targetType" value="egovframework.brte.sample.domain.trade.CustomerCredit" /> </bean> </property> </bean> </property> </bean> |
| 전자정부 EgovFlatFileItemReader | <bean id="itemReader" class="org.springframework.batch.item.file.FlatFileItemReader" scope="step"> <property name="resource" value="#{jobParameters[inputFile]}" /> <property name="lineMapper"> <bean class="egovframework.brte.core.item.file.mapping.EgovDefaultLineMapper"> <property name="lineTokenizer"> <bean class="egovframework.brte.core.item.file.transform.EgovDelimitedLineTokenizer"> <property name="delimiter" value=","/> </bean> </property> <property name="objectMapper"> <bean class="egovframework.brte.core.item.file.mapping.EgovObjectMapper"> <property name="type" value="egovframework.brte.sample.domain.trade.CustomerCredit" /> <property name="names" value="name,credit" /> </bean> </property> </bean> </property> </bean> |
| EgovFlatFileItemReader 설정항목 | 내용 | 예시 |
|---|---|---|
| delimiter | 필드의 경계를 구별해주는 문자를 나타낸다. | , (콤마) |
| type | VO 클래스를 나타낸다. | org.springframework.batch.CustomerCredit |
| names | VO 클래스의 필드를 나타낸다. | name,credit |
읽어들인 문자열에서 필드의 경계를 파일 내의 문자열 길이로 판단하여, 필드를 분리한다.
| 구분 | 설정 |
|---|---|
| 스프링 FlatFileItemReader | <bean id="itemReader" class="org.springframework.batch.item.file.FlatFileItemReader" scope="step"> <property name="resource" value="#{jobParameters[inputFile]}" /> <property name="lineMapper"> <bean class="org.springframework.batch.item.file.mapping.DefaultLineMapper"> <property name="lineTokenizer"> <bean class="org.springframework.batch.item.file.transform.FixedLengthTokenizer"> <property name="columns" value="1-9,10-11" /> <property name="names" value="name,credit" /> </bean> </property> <property name="fieldSetMapper"> <bean class="org.springframework.batch.item.file.mapping.BeanWrapperFieldSetMapper"> <property name="targetType" value="egovframework.brte.sample.domain.trade.CustomerCredit" /> </bean> </property> </bean> </property> </bean> |
| 전자정부 EgovFlatFileItemReader | <bean id="itemReader" class="org.springframework.batch.item.file.FlatFileItemReader" scope="step"> <property name="resource" value="#{jobParameters[inputFile]}" /> <property name="lineMapper"> <bean class="egovframework.brte.core.item.file.mapping.EgovDefaultLineMapper"> <property name="lineTokenizer"> <bean class="egovframework.brte.core.item.file.transform.EgovFixedLengthTokenizer"> <property name="columns" value="1-9,10-11" /> </bean> </property> <property name="objectMapper"> <bean class="egovframework.brte.core.item.file.mapping.EgovObjectMapper"> <property name="type" value="egovframework.brte.sample.domain.trade.CustomerCredit" /> <property name="names" value="name,credit" /> </bean> </property> </bean> </property> </bean> |
| EgovFlatFileItemReader 설정항목 | 내용 | 예시 |
|---|---|---|
| column | 필드 경계의 범위를 나타낸다. | 1-9,10-11 |
| type | VO 클래스를 나타낸다. | org.springframework.batch.CustomerCredit |
| names | VO 클래스의 필드를 나타낸다. | name,credit |
전자정부에서는 EgovFixedByteLengthTokenizer를 추가적으로 제공한다. EgovFixedByteLengthTokenizer는 기본적으로 FixedLengthTokenizer와 유사하나, byte 문자열을 기준으로 필드의 경계값을 구해 필드를 분리한다.
| 구분 | 설정 |
|---|---|
| 전자정부 EgovFlatFileItemReader | <bean id="itemReader" class="org.springframework.batch.item.file.FlatFileItemReader" scope="step"> <property name="resource" value="#{jobParameters[inputFile]}" /> <property name="lineMapper"> <bean class="egovframework.brte.core.item.file.mapping.EgovDefaultLineMapper"> <property name="lineTokenizer"> <bean class="egovframework.brte.core.item.file.transform.EgovFixedByteLengthTokenizer"> <property name="byteEncoding" value="utf-8"/> <property name="columns" value="1-9,10-11" /> </bean> </property> <property name="objectMapper"> <bean class="egovframework.brte.core.item.file.file.mapping.EgovObjectMapper"> <property name="type" value="egovframework.brte.sample.domain.trade.CustomerCredit" /> <property name="names" value="name,credit" /> </bean> </property> </bean> </property> </bean> |
| EgovFlatFileItemReader 설정항목 | 내용 | 예시 |
|---|---|---|
| column | 필드 경계의 길이를 나타낸다. | 1-9,10-11 |
| byteEncoding | byte 문자열의 인코딩 타입을 나타낸다. | utf-8 |
| type | VO 클래스를 나타낸다. | org.springframework.batch.CustomerCredit |
| names | VO 클래스의 필드를 나타낸다. | name,credit |
배치 Job 정의 시 Resource 엘리먼트의 shell step에 shell script에 포함된 파일명에서 일련번호(index)를 사용할 수 있는 Reader를 제공한다.
Index 파일명을 사용하면 파일의 일련번호를 기준으로 동적인 파일명 생성이 가능하다.
Index(NDX) 파일명 치환 로직
NDX File : 파일 이름이 “[이름]_NDX_[YYYYMMDDhhmmss]” 형식으로 이루어진 파일
ex) Sample_NDX_20121126151237
NDX 일련번호 : 파일명 끝에 14자리 수의 생성시간(년월일시분초)
NDX 파일명 치환 : “[이름]_NDX(Index)” 형식의 파일명은 해당 디렉터리의 NDX 파일에 대해 Index에 해당하는 실제 파일명으로 치환됨
(-2 ) : 일련번호 기준 마지막 파일에서 두 번째 이전 파일명으로 치환됨
(-1 ) : 일련번호 기준 마지막 파일에서 첫 번째 이전 파일명으로 치환됨
( 0 ) : 일련번호 기준 마지막 파일명으로 치환됨
(+1 ) : 일련번호 기준 마지막 파일에서 Index를 1 증가시켜 새로운 파일 생성
NDX 파일목록 중 잘못된 파일명이 존재할 경우 에러를 발생한다.
| 구분 | 예시 | 비고 |
|---|---|---|
| 에러 | Sample_NDX_20180104 | index 자릿수가 10자리 |
| 에러 | Sample_NDX_000A | index는 숫자만 허용함 |
| 무시 | Sample_20180104123456 | NDX 파일이 아닌 일반 파일로 인식 |
Property(indexResource)의 파일을 NDX파일 설정에 따라 읽어드린다.
<bean id="fileIndex-delimitedItemReader" class="egovframework.rte.bat.core.item.file.EgovIndexFileReader">
<property name="indexResource" value="file:./src/main/resources/egovframework/batch/data/inputs/csvData_NDX(0)" />
<property name="lineMapper">
<bean class="org.springframework.batch.item.file.mapping.DefaultLineMapper">
<property name="lineTokenizer">
<bean class="org.springframework.batch.item.file.transform.DelimitedLineTokenizer">
<property name="delimiter" value="," />
<property name="names" value="name,credit" />
</bean>
</property>
<property name="fieldSetMapper">
<bean class="org.springframework.batch.item.file.mapping.BeanWrapperFieldSetMapper">
<property name="targetType" value="egovframework.example.bat.domain.trade.CustomerCredit" />
</bean>
</property>
</bean>
</property>
</bean>
| EgovIndexFileReader 설정항목 | 내용 | 예시 |
|---|---|---|
| indexResource | 읽어올 index(NDX)파일을 설정한다. | Index(NDX) 파일명 치환 로직 참조 |
배치 처리시 Paging방식으로 mybatis에서 데이터를 읽기 위해 EgovMyBatisPagingItemReader 서비스를 제공합니다.(mybatis MyBatisPagingItemReader 클래스를 확장한 서비스) 실행환경 제공 Resource Variable, Step Variable, Job Variable 서비스와 함께 사용 가능하지만 parameterValues 서비스와 함께 사용은 불가능하다.

| 설정항목 | 내용 | 예시 |
|---|---|---|
| sqlSessionFactory | reader에 별도로 구현한 sessionFactory | sqlSession |
| parameterValues | 파라미터 전달을 위한 설정 | parameterValues |
| queryId | 네임스페이스를 가진 매퍼 파일을 Query Id. | EmpMapper.selectEmpList |
| scope | 해당 Reader가 적용될 Bean Scope | step, job |
| pageSize | 배치가 처리할 페이지 사이즈 크기 | #{100} |
| resourceVariable | 표준프레임워크 실행환경 Resource Variable 서비스를 사용하기 위한 설정 | resourceVariable |
| jobVariable | 표준프레임워크 실행환경 Step Variable 서비스를 사용하기 위한 설정 | jobVariable |
| stepVariable | 표준프레임워크 실행환경 Job Variable 서비스를 사용하기 위한 설정 | stepVariable |
<bean id="mybatisJobStep.mybatisItemReader" class="egovframework.rte.bat.item.database.EgovMyBatisPagingItemReader" scope="step">
<property name="sqlSessionFactory" ref="sqlSession" />
<property name="resourceVariable" ref="resourceVariable" />
<property name="jobVariable" ref="jobVariable" />
<property name="stepVariable" ref="stepVariable" />
<property name="queryId" value="EmpMapper.selectEmpList" />
<property name="pageSize" value="#{100}" />
</bean>
ItemWriter는 대상 타입에 관계없이 한번에 항목의 묶음(Chunk)을 쓰는 동작의 인터페이스이다.
ItemWriter의 기능은 ItemReader와 유사하지만 정반대의 동작을 한다. 기본적인 ItemWriter 인터페이스는 아래와 같다.
public interface ItemWriter<T> {
void write(List<? extends T> items) throws Exception;
}
write() 메소드는 ItemWriter의 필수적인 메소드이며 인자로 건넨 객체가 열려 있는 동안 쓰기 작업을 시도한다.
FlatFileItemWriter는 Resource, LineAggregator에 기본적으로 의존성을 갖으며, LineAggregator에 따라 구분자(Delimited)와 고정길이(Fixed Length) 방식으로 쓸 수 있다.

| 구분 | 데이터 형태 | 설명 |
|---|---|---|
| LineAggregator | Item → String | ItemReader, ItemProcessor 과정을 거친 Item을 1 라인의 String으로 변환하는 총 과정(FieldExtractor 과정을 포함한다.) - DelimitedLineAggregator (구분자) : Item의 Field와 Field 사이에 구분자를 삽입하여 1라인의 String으로 변환하는 과정 - FormatterLineAggregator(고정길이 포맷) : Item의 Field를 사용자가 설정한 포맷 기준으로 1라인의 String으로 변환하는 과정 |
| FieldExtractor | Item → Fields | Item에서 Field 값들을 꺼내어 Object[]로 변환하는 과정 |
아래 Delimited(구분자), Fixed Length(고정길이) 방식으로 설정한 FlatFileItemWriter의 예시를 통해 FlatFileItemWriter, LineAggregator, FieldExtractor의 의존 관계를 볼 수 있다.
| Aggregate 방식 | 설정 |
|---|---|
| Delimited (구분자) | <bean id="itemWriter" class="org.springframework.batch.item.file.FlatFileItemWriter" scope="step"> <property name="resource" value="#{jobParameters[outputFile]}" /> <property name="lineAggregator"> <bean class="org.springframework.batch.item.file.transform.DelimitedLineAggregator"> <property name="delimiter" value=","/> <property name="fieldExtractor"> <bean class="org.springframework.batch.item.file.transform.BeanWrapperFieldExtractor"> <property name="names" value="name,credit"/> </bean> </property> </bean> </property> </bean> |
| Fixed Length (고정길이) | <bean id="itemWriter" class="org.springframework.batch.item.file.FlatFileItemWriter" scope="step"> <property name="resource" value="#{jobParameters[outputFile]}" /> <property name="lineAggregator"> <bean class="org.springframework.batch.item.file.transform.FormatterLineAggregator"> <property name="format" value="%-9s%-2s" /> <property name="fieldExtractor"> <bean class="org.springframework.batch.item.file.transform.BeanWrapperFieldExtractor"> <property name="names" value="name,credit"/> </bean> </property> </bean> </property> </bean> |
BeanWrapperFieldExtractor에 아래와 같은 항목을 설정해야한다.
| 설정항목 | 내용 |
|---|---|
| names | VO 클래스의 필드를 나타낸다. |
✔ 사용하는 LineAggregator에 따라 설정항목이 다르므로 주의하여 설정해야 한다.
| LineAggregator | 설정항목 | 설명 | 설정 예 |
|---|---|---|---|
| DelimitedLineAggregator | delimiter | Item의 필드 값들을 1 Line의 String으로 만들 때 경계가 되는 구분자 지정 | ,(콤마) |
| FormatterLineAggregator | format | Item의 필드 값들을 1 Line의 String으로 만들 때 필드값의 형식과 고정길이 지정 | %-9s%-2s |
XML 쓰는 과정은 읽기 과정에 대칭적이다. StaxEventItemWriter는 Resource, marshaller, rootTagName가 필요하다. Java 객체는 marshaller에 전달되서 OXM 도구에 의해 각 fragment마다 StartDocument와 EndDocument 이벤트를 필터링하고 커스텀 이벤트 writer를 사용해 Resource를 쓰게 된다.
아래 XStreamMarshaller를 사용한 StaxEventItemWriter 설정 예가 있다.
<bean id="itemWriter" class="org.springframework.batch.item.xml.StaxEventItemWriter">
<property name="resource" ref="outputResource" />
<property name="marshaller" ref="customerCreditMarshaller" />
<property name="rootTagName" value="customers" />
<property name="overwriteOutput" value="true" />
</bean>
marshaller가 의존성 참조를 하고 있는 customerCreditMarshaller는 다음처럼 설정하면 된다.
<bean id="customerCreditMarshaller" class="org.springframework.oxm.xstream.XStreamMarshaller">
<property name="aliases">
<util:map id="aliases">
<entry key="customer" value="egovframework.brte.sample.domain.trade.CustomerCredit" />
<entry key="credit" value="java.math.BigDecimal" />
<entry key="name" value="java.lang.String" />
</util:map>
</property>
</bean>
플랫파일 및 XML 모두 특정 ItemWriter가 있지만 데이터베이스는 다르다. 왜냐하면 트랜잭션이 필요한 모든 기능을 제공하기 때문이다. 파일은 트랜잭션이 있는 것처럼 적절한 시점에 작성된 item을 추적하고 삭제하는 작업해야 하기 때문에 ItemWriter가 필요하다. 하지만 데이터베이스는 쓰기가 이미 트랜잭션에 포함되어 있기때문에 이 기능을 필요로 하지 않는다. 사용자는 ItemWriter 인터페이스를 구현해서 DAO를 만들거나 일반적인 처리 과정 관점에서 작성된 커스텀 ItemWriter 중 하나를 사용하면 된다.
아래 XML 설정은 JDBC를 이용한 JdbcBatchItemWriter의 설정 예시이다. JdbcCursorItemReader 설정과 마찬가지로 dataSouce 속성으로 DB connection을 넣어올 수 있는 datasource를 지정하고, sql 속성에 실행할 쿼리를 설정한다.
<bean id="itemWriter" class="org.springframework.batch.item.database.JdbcBatchItemWriter">
<property name="assertUpdates" value="true" />
<property name="itemSqlParameterSourceProvider">
<bean class="org.springframework.batch.item.database.BeanPropertyItemSqlParameterSourceProvider" />
</property>
<property name="sql" value="UPDATE CUSTOMER set credit = :credit where id = :id" />
<property name="dataSource" ref="dataSource" />
</bean>
아래 XML 설정은 iBatis를 이용한 IbatisBatchItemWriter의 설정 예시이다.
<bean id="itemWriter"
class="org.springframework.batch.item.database.IbatisBatchItemWriter">
<property name="statementId" value="updateCredit" />
<property name="sqlMapClient" ref="sqlMapClient" />
</bean>
sqlMapClient의 참조는 아래와 같다. configLocation 속성에 iBatis를 통해 쿼리를 작성해둔 파일의 경로를 설정한다.
<bean id="sqlMapClient" class="org.springframework.orm.ibatis.SqlMapClientFactoryBean">
<property name="dataSource" ref="dataSource" />
<property name="configLocation" value="ibatis-config.xml" />
</bean>
스프링 배치에서 제공하는 파일 기반 관련설정을 사용할 경우, 대용량 데이터 처리 시간이 상용 배치프레임워크 대비 성능이 떨어졌다. 이 문제를 해결하기 위해서 전자정부에서는 파일 ItemWriter의 요소 중 성능저하 요인인 LineAggregator 부분을 개선하여 제공한다
스프링에서 제공하는 BeanWrapperFieldExtractor를 경량화 한 EgovFieldExtractor와 FormatterLineAggreagotor의 기능을 경량화 한 EgovFixedLineAggregator를 제공한다.
| 스프링 FlatFileItemWriter 구조 | 전자정부 EgovFlatFileItemWriter 구조 |
|---|---|
![]() | ![]() |
| 개선사항 | 설명 |
|---|---|
| EgovFieldExtractor | 스프링에서 제공하는 BeanWrapperFieldExtractor를 개선하여 item에서 field 값을 추출하는 과정의 성능을 개선한 FieldExtractor 제공 |
| EgovFixedLineAggregator | 스프링에서 제공하는 FormatterLineAggregator는 Java의 format() 메소드를 이용하여 String을 다양한 format으로 변환할 수 있지만 가장 기본 설정인 문자열 길이만 지정할 때 성능이 떨어지는 단점이 있다. 따라서 사용자가 문자열 길이만 지정할 때를 고려해 경량화하여 성능의 초점을 둔 LineAggregator 제공 (format 지정이 필요한 경우, format을 VO에서 직접 적용하여 FormatterLineAggregator와 같은 기능이지만 성능 개선 된 EgovFixedLineAggregator 사용 가능) |
✔ 스프링에서 제공하는 FormatterLineAggregator, DelimitedLineAggregator와 EgovFieldExtractor는 동시 사용이 가능하므로 BeanWrapperFieldExtractor 대신 EgovFieldExtractor 사용 시, 보다 좋은 성능으로 write 할 수 있다.
BeanWrapperFieldExtractor, FormatterLineAggregator(or DelimitedLineAggregator)를 사용한 설정과 EgovFieldExtractor, EgovFixedLineAggregator(or DelimitedLineAggregator)를 사용한 FlatFileItemWriter 설정 비교는 아래와 같다.
| 구분 | 설정 |
|---|---|
| 스프링 FlatFileItemWriter | <bean id="itemWriter" class="org.springframework.batch.item.file.FlatFileItemWriter" scope="step"> <property name="resource" value="#{jobParameters[outputFile]}" /> <property name="lineAggregator"> <bean class="org.springframework.batch.item.file.transform.FormatterLineAggregator"> <property name="fieldExtractor"> <bean class="org.springframework.batch.item.file.transform.BeanWrapperFieldExtractor"> <property name="names" value="name,credit" /> </bean> </property> <property name="format" value="%-9s%-2s" /> </bean> </property> </bean> |
| 전자정부 EgovFlatFileItemWriter | <bean id="itemWriter" class="org.springframework.batch.item.file.FlatFileItemWriter" scope="step"> <property name="resource" value="#{jobParameters[outputFile]}" /> <property name="lineAggregator"> <bean class="egovframework.brte.core.item.file.transform.EgovFixedLengthLineAggregator"> <property name="fieldExtractor"> <bean class="egovframework.brte.core.item.file.transform.EgovFieldExtractor"> <property name="names" value="name,credit" /> </bean> </property> <property name="fieldRanges" value="9,2" /> </bean> </property> </bean> |
| EgovFlatFileItemWriter 설정항목 | 설명 |
|---|---|
| fieldRanges | Item의 필드 값들을 1 Line의 String으로 만들 때 필드값의 범위(고정길이) 지정 |
| names | VO 클래스의 필드를 나타낸다. |
| padding | 공백 패턴 설정 |
| 구분 | 설정 |
|---|---|
| 스프링 FlatFileItemWriter | <bean id="itemWriter" class="org.springframework.batch.item.file.FlatFileItemWriter" scope="step"> <property name="resource" value="#{jobParameters[outputFile]}" /> <property name="lineAggregator"> <bean class="org.springframework.batch.item.file.transform.DelimitedLineAggregator"> <property name="fieldExtractor"> <bean class="org.springframework.batch.item.file.transform.BeanWrapperFieldExtractor"> <property name="names" value="name,credit"/> </bean> </property> <property name="delimiter" value=","/> </bean> </property> </bean> |
| 전자정부 EgovFlatFileItemWriter | <bean id="itemWriter" class="org.springframework.batch.item.file.FlatFileItemWriter" scope="step"> <property name="resource" value="#{jobParameters[outputFile]}" /> <property name="lineAggregator"> <bean class="org.springframework.batch.item.file.transform.DelimitedLineAggregator"> <property name="fieldExtractor"> <bean class="egovframework.brte.core.item.file.transform.EgovFieldExtractor"> <property name="names" value="name,credit"/> </bean> </property> <property name="delimiter" value=","/> </bean> </property> </bean> |
| EgovFlatFileItemWriter 설정항목 | 설명 |
|---|---|
| delimiter | Item의 필드 값들을 1 Line의 String으로 만들 때 경계가 되는 구분자 지정 |
| names | VO 클래스의 필드를 나타낸다. |
사용하는 LineAggregator에 따라 설정항목이 다르다.
| LineAggregator | 설정항목 | 설명 | 설정 예 |
|---|---|---|---|
| DelimitedLineAggregator | delimiter | 필드의 경계를 구별해주는 문자 | , (콤마) |
| FormatterLineAggregator | format | 필드의 형식과 필드 경계의 범위 | %-9s%-2s |
| EgovFixedLengthLineAggregator | fieldRanges | 필드 경계의 범위 | 9,2 |
스프링에서 제공하는 JdbcBatchItemWriter는 사용자가 PreparedStatement를 setter하기 위한 클래스를 직접 작성하지 않고, XML 설정시 쿼리의 파라미터값을 지정만으로 자동으로 PreparedStatement를 setter해주는 기능을 제공한다. 하지만, 이 기능을 이용하면 대용량 데이터 처리 시간이 상용 배치프레임워크과 비교하여 큰 차이가 발생한다. 이러한 차이를 개선하고자 전자정부프레임워크에서는 EgovJdbcBatchItemWriter를 제공한다.
| 스프링 JdbcBatchItemWriter구조 | 전자정부 EgovJdbcBatchItemWriter 구조 |
|---|---|
![]() | ![]() |
자동으로 PreparedStatement를 setter 할 경우 JdbcBatchItemWriter는 BeanPropertyItemSqlParameterSourceProvider클래스를 사용하고 EgovJdbcBatchItemWriter는 EgovMethodMapItemPreparedStatementSetter클래스를 사용한다. 설정은 아래와 같다.
| 구분 | 설정 |
|---|---|
| JdbcBatchItemWriter | <bean id="itemWriter" class="org.springframework.batch.item.database.JdbcBatchItemWriter"> <property name="assertUpdates" value="true" /> <property name="itemSqlParameterSourceProvider"> <bean class="org.springframework.batch.item.database.BeanPropertyItemSqlParameterSourceProvider" /> </property> <property name="sql" value="UPDATE CUSTOMER set credit = :credit where id = :id" /> <property name="dataSource" ref="dataSource" /> </bean> |
| EgovJdbcBatchItemWriter | <bean id="itemWriter" class="egovframework.brte.core.item.database.EgovJdbcBatchItemWriter"> <property name="assertUpdates" value="true" /> <property name="itemPreparedStatementSetter"> <bean class="egovframework.brte.core.item.database.support.EgovMethodMapItemPreparedStatementSetter" /> </property> <property name="sql" value="UPDATE CUSTOMER set credit =? where id =?"/> <property name="params" value="credit,id"/> <property name="dataSource" ref="dataSource" /> </bean> |
✔ EgovMethodMapItemPreparedStatementSetter에는 파라미터의 값둘을 params의 value 값으로 설정한다.
보다 자세히 설명하면,
| writer | 설정 | 설명 |
|---|---|---|
| JdbcBatchItemWriter | 사용자가 작성한 class | 사용자가 setValues 메소드를 직접 작성하여 PreparedStatement의 데이터 설정 |
| BeanPropertyItemSqlParameterSourceProvider | sql에 파라미터 설정으로 PreparedStatement의 데이터 설정 | |
| EgovJdbcBatchItemWriter | 사용자가 작성한 class | 사용자가 setValues 메소드를 직접 작성하여 PreparedStatement의 데이터 설정 |
| EgovMethodMapItemPreparedStatementSetter | sql과 params의 설정으로 PreparedStatement의 데이터 설정 |
또한 EgovJdbcBatchItemWriter도 사용자가 직접 작성한 class를 PreparedStatementSetter로 설정할 수 있다. 클래스 작성시에는 EgovItemPreparedStatementSetter를 상속하여 사용한다. 아래의 EmployeeItemPreparedStatementSetter 클래스는 EgovItemPreparedStatementSetter를 상속받아서 사용자가 직접 작성한 것이다.
<bean id="itemWriter" class="org.springframework.batch.item.database.EgovJdbcBatchItemWriter">
<property name="itemPreparedStatementSetter">
<bean class="egovframework.brte.sample.example.support.EmployeeItemPreparedStatementSetter" />
</property>
<property name="sql“ value="update into UIP_EMPLOYEE (num, name, sex) values (?, ?, ?)" />
<property name="dataSource" ref="dataSource" />
</bean>
배치 Job 정의 시 Resource 엘리먼트의 shell step에 shell script에 포함된 파일명에서 일련번호(index)를 사용할 수 있는 Writer를 제공한다. Index 파일명을 사용하면 파일의 일련번호를 기준으로 동적인 파일명 생성이 가능하다.
Index(NDX) 파일명 치환 로직
NDX File : 파일 이름이 “[이름]_NDX_[YYYYMMDDhhmmss]” 형식으로 이루어진 파일
ex) Sample_NDX_20121126151237
NDX 일련번호 : 파일명 끝에 14자리 수의 생성시간(년월일시분초)
NDX 파일명 치환 : “[이름]_NDX(Index)” 형식의 파일명은 해당 디렉터리의 NDX 파일에 대해 Index에 해당하는 실제 파일명으로 치환됨
(-2 ) : 일련번호 기준 마지막 파일에서 두 번째 이전 파일명으로 치환됨
(-1 ) : 일련번호 기준 마지막 파일에서 첫 번째 이전 파일명으로 치환됨
( 0 ) : 일련번호 기준 마지막 파일명으로 치환됨
(+1 ) : 일련번호 기준 마지막 파일에서 Index를 1 증가시켜 새로운 파일 생성
NDX 파일목록 중 잘못된 파일명이 존재할 경우 에러를 발생한다.
| 구분 | 예시 | 비고 |
|---|---|---|
| 에러 | Sample_NDX_20180104 | index 자릿수가 10자리 |
| 에러 | Sample_NDX_000A | index는 숫자만 허용함 |
| 무시 | Sample_20180104123456 | NDX 파일이 아닌 일반 파일로 인식 |
Index Reader을 통해 읽어드린 파일을 NDX파일 설정에 따라 동적으로 새로운 파일을 생성한다.
<bean id="fileIndex-delimitedItemWriter" class="egovframework.rte.bat.core.item.file.EgovIndexFileWriter" scope="step">
<property name="indexResource" value="file:./src/main/resources/egovframework/batch/data/inputs/csvData_NDX(+1)" />
<property name="lineAggregator">
<bean class="org.springframework.batch.item.file.transform.DelimitedLineAggregator">
<property name="delimiter" value="," />
<property name="fieldExtractor">
<bean class="egovframework.rte.bat.core.item.file.transform.EgovFieldExtractor">
<property name="names" value="name,credit" />
</bean>
</property>
</bean>
</property>
</bean>
| EgovIndexFileWriter 설정항목 | 내용 | 예시 |
|---|---|---|
| indexResource | 설정된 index(NDX)에 따른 동적인 파일 생성 | Index(NDX) 파일명 치환 로직 참조 |
배치 처리시 mybatis에서 데이터를 쓰기 위해 EgovMyBatisBatchItemWriter 서비스를 제공합니다.(mybatis MyBatisBatchItemWriter 클래스를 확장한 서비스) 실행환경 제공 Resource Variable, Step Variable, Job Variable 서비스와 함께 사용 가능하다.

| 설정항목 | 내용 | 예시 |
|---|---|---|
| sqlSessionFactory | reader에 별도로 구현한 sessionFactory | sqlSession |
| statementId | 네임스페이스를 가진 매퍼 파일을 Query Id | EmpMapper.selectEmpList |
| resourceVariable | 표준프레임워크 실행환경 Resource Variable 서비스를 사용하기 위한 설정 | resourceVariable |
| jobVariable | 표준프레임워크 실행환경 Step Variable 서비스를 사용하기 위한 설정 | jobVariable |
| stepVariable | 표준프레임워크 실행환경 Job Variable 서비스를 사용하기 위한 설정 | stepVariable |
<bean id="mybatisJobStep.mybatisItemWriter" class="egovframework.rte.bat.item.database.EgovMyBatisBatchItemWriter">
<property name="resourceVariable" ref="resourceVariable" />
<property name="jobVariable" ref="jobVariable" />
<property name="stepVariable" ref="stepVariable" />
<property name="sqlSessionFactory" ref="sqlSession" />
<property name="statementId" value="EmpMapper.updateEmp" />
</bean>
사용자 정의 리소스 변수 선언 후 Setp에서 ItemReader, ItemWriter에서 사용자 정의 리소스를 사용할 수 있도록 EgovResourceVariable를 통해서 지원한다.

배치실행환경에서 제공하는 EgovResourceVariable 사용하여 사용자 정의 리소스를 설정한다.
<bean id="egovResourceVariable" class="egovframework.rte.bat.support.EgovResourceVariable">
<property name="pros">
<props>
<prop key="input.resource">file:./src/main/resources/egovframework/batch/data/inputs/csvData.csv</prop>
<prop key="writer.resource">file:./target/test-outputs/csvOutput_ResourceVariable_#{new java.text.SimpleDateFormat('yyyyMMddHHmmssSS').format(new java.util.Date())}.csv</prop>
</props>
</property>
</bean>
Setp에서 ItemReader, ItemWriter 사용시 사용자 정의 리소스 변수를 사용하여 resource 설정이 가능하다.
<bean id="delimitedToDelimitedJob-ResourceVariable.delimitedToDelimitedStep.delimitedItemReader"
class="org.springframework.batch.item.file.FlatFileItemReader" scope="step">
<property name="resource" value="#{egovResourceVariable.getVariable('input.resource')}" />
<property name="lineMapper">
<bean class="egovframework.rte.bat.core.item.file.mapping.EgovDefaultLineMapper">
<property name="lineTokenizer">
<bean class="egovframework.rte.bat.core.item.file.transform.EgovDelimitedLineTokenizer">
<property name="delimiter" value="," />
</bean>
</property>
<property name="objectMapper">
<bean class="egovframework.rte.bat.core.item.file.mapping.EgovObjectMapper">
<property name="type"
value="egovframework.example.bat.domain.trade.CustomerCredit" />
<property name="names" value="name,credit" />
</bean>
</property>
</bean>
</property>
</bean>
<bean id="delimitedToDelimitedJob-ResourceVariable.delimitedToDelimitedStep.delimitedItemWriter"
class="org.springframework.batch.item.file.FlatFileItemWriter" scope="step">
<property name="resource" value="#{egovResourceVariable.getVariable('writer.resource')}" />
<property name="lineAggregator">
<bean class="org.springframework.batch.item.file.transform.DelimitedLineAggregator">
<property name="delimiter" value="," />
<property name="fieldExtractor">
<bean class="egovframework.rte.bat.core.item.file.transform.EgovFieldExtractor">
<property name="names" value="name,credit" />
</bean>
</property>
</bean>
</property>
</bean>
JobRepository는 배치 작업 중의 정보를 저장하는 역할을 한다. 어떠한 Job이 언제 수행되었고, 언제 끝났으며, 몇 번이 실행되었고 실행에 대한 결과가 어떤지 등의 배치 작업의 수행과 관련된 모든 meta data가 저장되어 있다.
JobRepository은 수행되는 Job에 대한 정보를 담고 있는 저장소로 배치작업의 지속성 메커니즘이다. JobRepository는 Spring Batch에서 JobExecution와 StepExecution 등과 같은 지속성을 가진 정보의 기본 CRUD작업에 사용된다. 배치작업이 처음 실행되면 JobRepository에서 JobExecution이 생성되고 배치작업이 실행되는 동안 StepExecution 및 JobExecution의 정보들이 JobRepository에 저장되고 갱신되어 지속된다.
JobRepository는 배치 네임 스페이스를 통해서나 JobRepositoryFactoryBean 클래스를 사용하여 아래와 같이 설정할 수 있다.
<job-repository id="jobRepository"
data-source="dataSource"
transaction-manager="transactionManager"
isolation-level-for-create="SERIALIZABLE"
table-prefix="BATCH_"
max-varchar-length="1000"
/>
<bean id="jobRepository" class="org.springframework.batch.core.repository.support.JobRepositoryFactoryBean"
p:dataSource-ref="dataSource" p:transactionManager-ref="transactionManager" p:isolation-level-for-create="ISOLATION_DEFAULT"
p:table-prefix="BATCH_" p:max-var-char-length="1000"/>
네임 스페이스를 사용하는 경우, transactional advice가 자동으로 Repository 주위에 생성된다. 배치작업의 실패 후 다시 시작할 필요있는가의 상태를 포함하고 있는 메타 데이터가 제대로 지속되어 있는지 확인한다. repository이 트랜잭션 처리를 하지 않는다면 프레임 워크의 처리가 잘 정의되지 않는다. 기본적인 isolation level은 가장 격리 수준이 높은 serializable이고 재정의도 가능하다.
<job-repository id="jobRepository"
isolation-level-for-create="REPEATABLE_READ" />
네임스페이스나 Bean 정의를 사용하지 않는 경우에는 AOP를 이용하여 Repository에 대한 트랜잭션의 관리할 수 있도록 설정해야 한다.
<aop:config>
<aop:advisor
pointcut="execution(* org.springframework.batch.core..*Repository+.*(..))"/>
<advice-ref="txAdvice" />
</aop:config>
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes>
<tx:method name="*" />
</tx:attributes>
</tx:advice>
위의 설정은 거의 변경없이 사용할 수 있다.다만, 네임 스페이스의 선언을 포함하고 있는지 Spring-aop, Spring-tx이 classpath에 있는지 확인하여야 한다.
JobRepository는 메타 데이터 테이블의 테이블 접두사 수정도 가능하다. 기본적으로 모든 데이터 테이블은 BATCH_JOB_EXECUTION 와 BATCH_STEP_EXECUTION 등과 같이 BATCH_로 시작한다.하지만 테이블명 앞에 스키마명을 추가하거나, 같은 스키마 내에서 메타 데이터 테이블의 하나 이상의 세트가 필요하면 테이블 접두사 수정이 필요하다.
<job-repository id="jobRepository"
table-prefix="SYSTEM.TEST_" />
위와 같이 설정한다면 모든 쿼리에 메타 데이터 테이블명이 SYSTEM.TEST_”로 시작된다. BATCH_JOB_EXECUTION은 SYSTEM.TEST_JOB_EXECUTION으로 변경될 것이다.
✔오직 테이블의 접두사만 변경가능하다. 테이블명과 컬럼명은 수정할 수 없다.
Spring 배치는 jobRepository를 데이터베이스가 아닌 메모리로 설정할 수 있다. 작업에 대한 상태를 유지하지 않아도 되는 배치작업의 도메인 개체를 데이터 베이스에 저장할 경우, 각각의 커밋 시점에 추가 시간이 걸린다. 이 경우, 메모리 Repository를 통해 잡을 실행한다.
<bean id="jobRepository"
class="org.springframework.batch.core.repository.support.MapJobRepositoryFactoryBean">
<property name="transactionManager" ref="transactionManager"/>
</bean>
메모리는 JVM 인스턴스가 다시 시작하는 것을 허용하지 않으며, 휘발성을 가진 jobRepository이다. 또한 동일한 매개 변수 두 작업 인스턴스가 동시에 실행되는 것을 보장 할 수 없다. multi-threaded 배치작업이나 파티션 작업에 적합하지 않을 수 있다.그러므로 JobRepository는 데이터베이스을 사용하여야 한다.
하지만 repository내에서 rollback이 있으므로 트랜잭션 manager의 설정이 필요하다.그리고 테스트를 위해 많은 사람들이 비즈니스 논리상 여전히 트랜잭션이 있기 때문에 ResourcelessTransactionManager가 유용하다.
Spring Batch에서 지원되는 데이터베이스 목록이외의 데이터베이스를 사용하는 경우,비슷하게 지원하는 하는 데이터베이스가 있다면 그것을 사용할 수 있다. 이 작업을 수행하려면 네임 스페이스 사용하는 대신에 JobRepositoryFactoryBean를 사용하여 가장 가깝게 일치하는 데이터베이스 유형을 설정 할 수 있다
<bean id="jobRepository" class="org.springframework.batch.core.repository.support.JobRepositoryFactoryBean">
<property name="databaseType" value="db2"/>
<property name="dataSource" ref="dataSource"/>
</bean>
주의
✔ altibase나 tibero는 지원되는 데이터베이스 타입이 아니다. altibase나 tibero 연결시에는 jobRepository에 databaseType으로 oracle을 추가 설정해야 한다.
<bean id="jobRepository"
class="org.springframework.batch.core.repository.support.JobRepositoryFactoryBean"
p:dataSource-ref="dataSource" p:databaseType="oracle" p:transactionManager-ref="transactionManager" p:lobHandler-ref="lobHandler"/>
JobLauncher는 배치작업을 실행시키는 역할을 한다. Job과 Job Parameters를 이용하여 요청된 배치 작업을 수행한 후 JobExecution을 반환한다.
JobLauncher 인터페이스를 보면 Job과 Job Parameter를 이용하여 요청된 Job을 수행한 후 JobExecution을 반환되는 run메소드가 정의되어 있다.
public interface JobLauncher {
public JobExecution run(Job job, JobParameters jobParameters) throws JobExecutionAlreadyRunningException,
JobRestartException, JobInstanceAlreadyCompleteException, JobParametersInvalidException;
}
JobLauncher 인터페이스의 기본 구현 클래스로는 SimpleJobLauncher이 제공된다. SimpleJobLauncher클래스는 JobName과 JobParameter를 이용하여 JobRepository에서 Job의 실행시도를 나타내는 JobExecution을 획득하고 작업을 수행한다.
이를 이용한 jobLauncher 설정은 아래와 같다. JobExecution을 획득하기 위한 jobRepository의 설정이 필수이다.
<bean id="jobLauncher" class="org.springframework.batch.core.launch.support.SimpleJobLauncher">
<property name="jobRepository" ref="jobRepository" />
</bean>
JobLauncher는 taskExecutor 설정을 통해 Job을 동기적, 혹은 비동기적으로 실행할 수 있다. 별도로 설정하지 않으면 syncTaskExecutor클래스가 디폴트로 설정되어 동기적으로 아래와 같이 실행된다. client에게 배치작업의 요청을 받게 되면 JobLauncher는 하나의 JobExecution을 획득하고, 그것을 배치작업을 실행하는 메소드에 전달하여 최종적으로 배치 작업 후 Client에게 JobExecution을 반환한다.

위의 흐름은 간단하며 스케줄러에서 실행하면 잘 동작하지만, HTTP 요청에서 시작하려고 할 때 문제가 발생한다. 배치 작업의 특성상 처리시간이 오래 걸리는 작업이 많을 것이고,그 작업시간동안 HTTP 응답을 계속 기다리는 것은 좋지 않다. 이 경우에는 아래와 같이 SimpleJobLauncher가 Client에게 즉시 JobExecution을 반환하는 비동기식 동작 방법이 필요하다.

JobLauncher 설정에서 SimpleAsyncTaskExecutor클래스를 통해 비동기로도 쉽게 설정할 수 있다.
<bean id="jobLauncher" class="org.springframework.batch.core.launch.support.SimpleJobLauncher">
<property name="jobRepository" ref="jobRepository" />
<property name="taskExecutor">
<bean class="org.springframework.core.task.SimpleAsyncTaskExecutor" />
</property>
</bean>
Spring의 TaskExecutor 인터페이스의 모든 구현은 비동기로 실행하는 배치작업에 대한 제어의 목적으로 사용된다.
Remote JobLauncher는 온라인 상에서 별도의 배치 서버의 Batch Job작업을 실행시키는 역할을 한다. 온라인 상의 Client와 Server를 이용하여 요청된 배치 작업을 수행한다.
온라인상의 Remote JobLauncher를 구현하기 위하여 Hessian Binary Web Service를 사용한다. Hessian 웹서비스는 별도의 대형 프레임워크를 설치하지 않고도 간편하게 사용할 수 있은 웹서비스이며, HTTP기반의 경량 바이러리 프로토콜로 별도의 확장없이 바이너리 데이터를 전송하는데 적합하다. 또한, 스피링의 HessianProxyFactoryBean과 HessianServiceExporter를 사용하여 편리한 Integration을 지원한다.
Hessian을 사용하기 위하여 아래와 같이 라이브러리 디펜던시를 설정한다.
<dependency>
<groupId>com.caucho</groupId>
<artifactId>hessian</artifactId>
<version>4.0.38</version>
</dependency>
아래의 예제는 표준프레임워크 개발환경(v3.7)의 배치 템플릿 (SAM파일 Web 기반)을 사용하여 기존의 JobLauncher를 웹서비스에 등록하여 외부(Client)에서 호출하여 실행한다. 본 가이드의 실행 예제는 RemoteJobLauncher 예제에서 다운로드하여 확인할 수 있다.
Remote JobLauncher의 예제 작성 방법을 다음과 같다.
Web 서비스 (Server)
테스트 (Client)

Step 1 개발환경의 배치 템플릿 생성 개발환경의 “New Batch Template Project”를 이용하여 SAM파일 형식의 Web 기반 템플릿을 생성한다. 배치 템플릿 관련 자세한 사항은 배치 템플릿 위저드를 참조한다.
Step 2 Hessain Web Service의 라이브러리 등록 작성할 Batch JobLauncher를 온라인상에서 실행하기 위하여 바이너리 형식의 간편한 Web Service인 Hessian을 사용하며, 해당 라이브러리를 pom.xml에 등록한다.
<dependency>
<groupId>com.caucho</groupId>
<artifactId>hessian</artifactId>
<version>4.0.38</version>
</dependency>
Step 3 RemoteJobLaucher 작성 RemoteJobLauncher는 웹서비스에 호출할 수 있는 인터페이스이며 이를 통하여 작성된 배치를 실행하고, 또한 필요한 경우 결과값을 넘겨주는 역활을 한다. 작성된 RemoteJobLauncher를 HessianServiceExporter에 등록하기 위하여 인터페이스 클래스와 구현체(Implement)클래스를 작성하여야 한다.
package egovframework.example.bat.remote;
import java.util.HashMap;
public interface RemoteJobLauncher {
public HashMap<String,Object> callRemoteBatchRunner(String jobName);
}
package egovframework.example.bat.remote;
import java.util.ArrayList;
...
import egovframework.rte.bat.core.launch.support.EgovBatchRunner;
public class RemoteJobLauncherImpl implements RemoteJobLauncher {
...
@Override
public HashMap<String,Object> callRemoteBatchRunner(String jobName) {
//배치 템플릿의 BatchRunController.java의 batchRun메소드를 참조한다.
...
return resultMap;
}
Step 4 RemoteJobLauncher로 빈생성 및 서블릿 등록
<bean name="/EgovJobLauncher.remote" class="org.springframework.remoting.caucho.HessianServiceExporter">
<property name="service" ref="remoteJobLauncher" />
<property name="serviceInterface" value="egovframework.example.bat.remote.RemoteJobLauncher" />
</bean>
<bean id="remoteJobLauncher" class="egovframework.example.bat.remote.RemoteJobLauncherImpl" />
** 웹서비스 호출 예시 **
http://<domain_address>/<context_path>/EgovJobLauncher.remote
<servlet>
<servlet-name>remoting</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/config/egovframework/springmvc/hessian-servlet.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>remoting</servlet-name>
<url-pattern>*.remote</url-pattern>
</servlet-mapping>
Remote 배치 서비스를 호출하기 위하여 스프링의 HessianProxyFactoryBean을 사용하며 배치 실행 후 결과 값으로 배치 실행정보를 출력한다.
Step 1 HessainProxyFactoryBean에 RemoteJobLanucher를 등록한다. 인자값으로 serviceUrl을 지정할 수 있으며, serviceInterface를 지정하여 런처의 메소드를 실행할 수 있다.
<bean id="remoteHessianJobLauncher"
class="org.springframework.remoting.caucho.HessianProxyFactoryBean">
<property name="serviceUrl"
value="http://localhost:8080/egovframework.example.bat.template.sam.web/EgovJobLauncher.remote" />
<property name="serviceInterface" value="egovframework.example.bat.remote.RemoteJobLauncher" />
</bean>
Step 2 테스트 파일을 작성하여 등록된 웹서비스를 호출한다. 아래와 같이 테스트 파일을 작성하여 웹서비스를 호출하며 템플릿 예제의 “delimitedToDelimitedJob” 배치 job을 실행하여 결과로 Job Instance 및 step Info를 출력한다.
package egovtest.webBatchRemote;
import org.springframework.context.ApplicationContext;
...
import egovframework.example.bat.remote.RemoteJobLauncher;
public class RemoteJobClient_test {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("client-beans-remote.xml");
RemoteJobLauncher remoteJobLauncher = (RemoteJobLauncher) context.getBean("remoteHessianJobLauncher");
String jobName = "delimitedToDelimitedJob";
HashMap<String,Object> map = (HashMap<String, Object>)remoteJobLauncher.callRemoteBatchRunner(jobName);
HashMap<String, Object> jobInstances = (HashMap<String, Object>) map.get("jobInstances");
List<HashMap<String, Object>> stepsInfo = (ArrayList<HashMap<String, Object>>)map.get("stepsInfo");
System.out.println("[jobInstance] ===============================================================");
System.out.println("job id = "+jobInstances.get("id"));
System.out.println("job name = "+jobInstances.get("name"));
System.out.println("parameters = "+jobInstances.get("parameters"));
System.out.println("startTime = "+jobInstances.get("startTime"));
System.out.println("endTime = "+jobInstances.get("endTime"));
System.out.println("isRunning = "+ ((boolean)jobInstances.get("isRunning") ? "Running" : "Ready"));
System.out.println("exitStatus = "+jobInstances.get("exitStatus"));
System.out.println("[stepsInfo] ===============================================================");
for(HashMap<String,Object> stepInfo : stepsInfo) {
System.out.println("stepId = "+stepInfo.get("stepId"));
System.out.println("stepName = "+stepInfo.get("stepName"));
System.out.println("readCount = "+stepInfo.get("readCount"));
System.out.println("writeCount = "+stepInfo.get("writeCount"));
System.out.println("readSkipCount = "+stepInfo.get("readSkipCount"));
System.out.println("processSkipCount = "+stepInfo.get("processSkipCount"));
System.out.println("writeSkipCount = "+stepInfo.get("writeSkipCount"));
System.out.println("totalSkipCount = "+stepInfo.get("totalSkipCount"));
System.out.println("commitCount = "+stepInfo.get("commitCount"));
System.out.println("rollbackCount = "+stepInfo.get("rollbackCount"));
System.out.println("exitStatus = "+stepInfo.get("exitStatus"));
}
}
}
출력 결과
[jobInstance] ===============================================================
job id = 0
job name = delimitedToDelimitedJob
parameters = {inputFile=classpath:/egovframework/batch/data/inputs/csvData.csv, outputFile=file:xxxxx/remote-batch-output/csvOutput_xxxx.csv, timestamp=xxxxxxxxxxx}
startTime = 201x-xx-xx xx:xx:xx.xxx
endTime = 201x-xx-xx xx:xx:xx.xxx
isRunning = Ready
exitStatus = COMPLETED
[stepsInfo] ===============================================================
stepId = 0
stepName = delimitedToDelimitedStep
readCount = 4
writeCount = 4
readSkipCount = 0
processSkipCount = 0
writeSkipCount = 0
totalSkipCount = 0
commitCount = 3
rollbackCount = 0
exitStatus = COMPLETED
JobRunner는 외부 실행 모듈과 JobLauncher를 연결해주는 모듈로, 용도에 맞게 구현이 필요하다. 전자정부 표준프레임워크에서는 작업실행 유형에 따라 미리 JobRunner를 미리 구현한 표준 Batch Runner를 제공한다.
배치작업의 실행 유형에 따라 아래와 같이 3가지의 Batch Runner를 제공한다.
각 Batch Runner가 제공하는 기능은 아래와 같다.
| Batch Runner 종류 | Java Application 실행 | Web 실행 | Job 상태 모니터링 | Scheduling 기능 | 명령 프롬프트 연동 지원 |
|---|---|---|---|---|---|
| EgovBatchRunner | O | O | O | X | △ |
| EgovCommandLineRunner | O | X | O | X | O |
| EgovSchedulerRunner | O | O | X | O | △ |
✔ EgovBatchRunner, EgovSchedulerRunner에서 명령 프롬프트 연동을 위해서는 추가적인 구현이 필요하다.
EgovBatchRunner를 이용하여 Job Operator 및 Job Explorer를 이용하여 Job Config에 등록된 Job을 실행하고, 실행 상태를 변경할 수 있다. 또한 Job Repository에 접근할 수 있는 기능을 제공한다.

EgovBatchRunner를 사용하기 위해서는 XML 파일에 JobOperator, JobExplorer 그리고 JobRepository가 정의되어야 한다. 그리고 JobOperator를 생성하기 위해서는 JobLauncher와 JobRegistry의 정의가 필요하다.
<bean id="jobOperator"
class="org.springframework.batch.core.launch.support.SimpleJobOperator"
p:jobLauncher-ref="jobLauncher" p:jobExplorer-ref="jobExplorer"
p:jobRepository-ref="jobRepository" p:jobRegistry-ref="jobRegistry" />
<bean id="jobLauncher"
class="org.springframework.batch.core.launch.support.SimpleJobLauncher">
<property name="jobRepository" ref="jobRepository" />
</bean>
<bean id="jobRepository"
class="org.springframework.batch.core.repository.support.JobRepositoryFactoryBean"
p:dataSource-ref="egov.dataSource" p:transactionManager-ref="transactionManager"
p:lobHandler-ref="lobHandler" />
<bean id="jobExplorer"
class="org.springframework.batch.core.explore.support.JobExplorerFactoryBean"
p:dataSource-ref="egov.dataSource" />
<bean id="jobRegistry"
class="org.springframework.batch.core.configuration.support.MapJobRegistry" />
EgovBatchRunner의 생성자에 JobOperator, JobExplorer, JobRepository를 전달한다.
<bean id="jobBatchRunner" class="egovframework.brte.core.launch.support.EgovBatchRunner">
<constructor-arg ref="jobOperator" />
<constructor-arg ref="jobExplorer" />
<constructor-arg ref="jobRepository" />
</bean>
EgovBatchRunner에서는 Job Operator, Job Explorer에서 제공하는 주요 메소드를 기반으로 하여 Job Config에 등록된 Job 이름 조회, Job 시작, 재시작, 정지 등의 기능을 제공한다. Job Repository는 메소드를 직접 제공하지 않는 대신, 필요에 따라 Job Repository 객체에 접근할 수 있도록 하였다.
| 메소드명 | 설명 | 파라미터 |
|---|---|---|
| start | 각각 문자열 형태의 Job 이름, Job Parameter를 이용하여 Job을 시작한다. 이 때, Job Parameter는 기존에 실행했던 Job Parameter와 다른 고유한 값을 가져야 한다. | Job 이름, Job Parameter |
| restart | Job의 Execution ID를 이용하여, 정지되었거나 이미 종료 된 Job 중 재실행 가능한 Job 을 재시작한다. | Job의 Execution ID |
| stop | Job의 Execution ID를 이용하여, 실행 중인 Job을 정지시킨다. | Job의 Execution ID |
EgovBatchRunner에서 사용할 Job Parameter를 생성하기 위해서 제공하는 메소드로, Job Parameter를 문자열 형태로 생성한다.
| 메소드명 | 설명 | 파라미터 |
|---|---|---|
| createUniqueJobParameters | Timestamp를 이용하여 유일한 값을 지니는 Job Parameter 문자열을 생성한다. | 없음 |
| addJobParameter | 이미 생성된 Job Parameter 문자열에 Job Parameter 문자열을 추가한다. | 기존 Job Parameter 변수 ,추가할 Job Parameter 이름(키), 값 , |
Job Parameter 생성 예제는 아래와 같다.
String jobParameters = egovBatchRunner.createUniqueJobParameters();
jobParameters = egovBatchRunner.addJobParameter(jobParameters, "inputFile", "/egovframework/batch/data/inputs/csvData.csv");
이렇게 생성된 JobParameters는 XML에서 사용할 수 있다. 자세한 내용은 Job Parameters 항목을 참조한다.
EgovBatchRunner 예제
EgovCommandLineRunner는 Job Launcher 및 Job Explorer를 이용하여 Job Config에 등록된 Job을 실행할 수 있으며, 실행할 수 있도록 하는 기능을 제공한다.

EgovCommandLineRunner에서는 start 메소드를 이용하여 Job을 시작한다. start 메소드에 필요한 파라미터는 아래와 같으며, 배치실행을 위해서는 Job Path와 Job Identifier는 반드시 필요하다.
| 파라미터 | 설명 |
|---|---|
| Job Path | Job 실행에 필요한 context 정보가 들어있는 xml |
| Job Identifier | 실행할 Job의 이름, 혹은 Job의 Execution ID |
| Parameters | Job Parameter |
| Option | 실행옵션 |
실행옵션을 지정하지 않았을 경우 Job을 시작한다. 그리고 실행옵션을 지정했을 경우 해당하는 동작을 수행하며, Job Identifier의 지정 방식도 달라진다. 실행옵션의 종류 및 Job Identifier 지정방식은 아래와 같다.
| 실행옵션 | 설명 | Job Identifier |
|---|---|---|
| 미지정 | Job을 시작한다. | Job의 이름 |
| -restart | 정지되었거나 이미 종료 된 Job 중 재실행 가능한 Job 을 재시작한다. | Job의 Execution ID |
| -stop | 실행 중인 Job을 정지시킨다. | Job의 Execution ID |
| -next | Job을 JobParameter만 변경하여 실행한다. | Job의 Execution ID |
| -abandon | 정지된 Job의 상태를 “ABANDONED”으로 변경한다. | Job의 Execution ID |
배치 템플릿을 이용한 EgovCommandLineRunner 예제
기존의 Batch Runner와는 다르게, EgovSchedulerRunner는 Job을 직접 실행하는 것이 아니라 Scheduler를 실행한다. 이 Scheduler가 설정되어 있는 시간 및 주기 간격으로 Job을 실행하게 된다. Scheduler는 Quartz를 사용하고 있으며, Quartz의 자세한 사용법 및 설정 방법은 Scheduling 서비스를 참고한다.

JobRegistry는 생성된 Job을 자동으로 Map형태로 저장하여 관리(추가, 삭제 등)한다.
JobRegistry는 필수는 아니지만 context에서 Job을 추적하거나 다른 곳에서 생성된 Job을 application context의 중앙에 모을 때 유용하다. 등록된 Job의 이름과 속성들을 조작할 수 있으며 job name과 job instance의 Map의 형태로 이루워져 있다.
<bean id="jobRegistry" class="org.springframework.batch.core.configuration.support.MapJobRegistry" />
JobRegistry에 Job을 자동으로 등록하는 방법은 두 가지가 있다.
이것은 Bean post-processor으로 Application Context가 올라가면서 bean 등록 시, 자동으로 JobRegistry에 Job을 등록 시켜준다.
<bean id="jobRegistryBeanPostProcessor" class="org.springframework.batch.core.configuration.support.JobRegistryBeanPostProcessor">
<property name="jobRegistry" ref="jobRegistry"/>
</bean>
부모 context에서 자식 contexts에 생성된 Job들을 사용하기 위해 자동으로 부모 context의 JobRegistry에 Job들을 등록시킨다. AutomaticJobRegistrar을 사용하기 위해서는 아래와 같이 ApplicationContextFactory와 JobLoader의 설정은 필수이다. ApplicationContextFactory는 ClassPathXmlApplicationContextFactory를 이용하여 자식 context를 생성하고 JobLoader는 자식 context를 관리하고 JobRegistry에 Job 등록을 한다.
<bean class="org.spr...AutomaticJobRegistrar">
<property name="applicationContextFactories">
<bean class="org.spr...ClasspathXmlApplicationContextsFactoryBean">
<property name="resources" value="classpath*:/config/job*.xml" />
</bean>
</property>
<property name="jobLoader">
<bean class="org.spr...DefaultJobLoader">
<property name="jobRegistry" ref="jobRegistry" />
</bean>
</property>
</bean>
Skip, Retry, Repeat은 효율적인 배치수행을 위해 필요한 기능들이다. Repeat 정책에 따라 Step과 Chunk가 반복적으로 수행되면서 데이터 Read, Process, Write 과정이 일어나는데, 여기서 Skip과 Retry 이용해 배치작업을 효율적으로 수행할 수 있다. 각 기능이 쓰이는 위치는 다음 그림을 참고한다.

Skip은 데이터를 처리하는 동안 설정된 Exception이 발생했을 경우, 해당 데이터 처리를 건너뛰는 기능이다. 데이터의 사소한 오류에 대해 Step의 실패처리 대신 Skip을 함으로써, 배치수행의 빈번한 실패를 줄일 수 있게 한다.
Skip설정은 Job설정파일의 <chunk> 내부에서 이루어진다. 일반 chunk 설정(reader, processor, writer, commit-interval)에 추가로 skip-limit 을 지정한다. 또한 <skippable-exception-classes>를 지정할 수 있으며 (이것은 옵션?) 상세설명은 다음 표를 참고한다.
| 항목 | 설명 | |
|---|---|---|
| skip-limit | Skip 할 수 있는 최대 횟수를 지정 default=0 이므로 꼭 지정해줘야 Skip기능 사용 할 수 있음 (확인필요) | |
| <skippable-exception-classes> | <include> | skip 해야하는 Exception 범위를 지정 |
| <skippable-exception-classes> | <exclude> | include로 지정한 exception의 하위 exception 중, Skip하지 않을 Exception 지정 |
✔ <skippable-exception-classes> 항목의 Exception 범위 지정은 데이터 성격에 대해 잘 알고 있는 사람이 결정해야 한다.
예를 들어 공급업체의 데이터 처리는 Skip 하도록 설정할 수 있지만 금융거래에서 데이터 처리는 Skip이 되어선 안되기 때문이다.
다음 예시는 FlatFileItemReader로 데이터를 읽는 과정에서 <include> 로 설정된 FlatFileParseException 발생 시 Skip이 일어나도록 설정이 되어 있고, 이렇게 발생한 Skip은 10번까지만 허용한다. 그 이상의 Skip이 발생한다면 Step을 실패처리한다.
<step id="step1">
<tasklet>
<chunk reader="flatFileItemReader" writer="itemWriter" commit-interval="10" skip-limit="10">
<skippable-exception-classes>
<include class="org.springframework.batch.item.file.FlatFileParseException"/>
</skippable-exception-classes>
</chunk>
</tasklet>
</step>
✔ 내부적으로 Skip의 횟수를 관리하는 Counter가 있는데 read, process, write 별로 분리되어 있으며, skip-limit에는 각 Counter의 합계가 적용된다. 데이터 성격에 따라 Skip관리를 위해 Counter를 유용하게 사용할 수 있다.
위 예시에서 한가지 문제가 있는데 FlatFileItemReader을 제외한 Exception이 발생하는 경우 Job을 실패로 처리하는 것이다. 이러한 처리가 옳을 수도 있지만, 다음예시 처럼 <exclude>를 설정하여 지정한 Exception클래스와 그 하위에러가 발생할 경우에 Skip하지 않고, 에러를 발생시키도록 표현하는 것이 명확하다.
<step id="step1">
<tasklet>
<chunk reader="flatFileItemReader" writer="itemWriter" commit-interval="10" skip-limit="10">
<skippable-exception-classes>
<include class="java.lang.Exception"/>
<exclude class="java.io.FileNotFoundException"/>
</skippable-exception-classes>
</chunk>
</tasklet>
</step>
Retry는 데이터를 Processing, Writing 하는 동안 설정된 Exception이 발생했을 경우, 지정한 정책에 따라 데이터 처리를 재시도하는 기능이다. Skip 과 마찬가지로 Retry를 함으로써, 배치수행의 빈번한 실패를 줄일 수 있게 한다.
Retry설정은 Job설정파일의 <chunk> 내부에서 이루어진다. 일반 chunk 설정(reader, processor, writer, commit-interval)에 추가로 retry-limit 을 지정한다. 또한 <retryable-exception-classes>를 지정할 수 있으며 상세설명은 하단 표를 참고한다.
| 항목 | 설명 | |
|---|---|---|
| retry-limit | Retry 할 수 있는 최대 횟수를 지정 | |
| <retryable-exception-classes> | <include> | Retry 해야하는 Exception 범위를 지정 |
| <retryable-exception-classes> | <exclude> | include로 지정한 exception의 하위 exception 중, Retry하지 않을 Exception 지정 |
✔ Item Processing과 Item Writing 과정에서만 Retry 된다.
데이터를 처리하는 Read과정에서 주로 발생하는 FlatFileParseException 대한 문제는 대부분 Skip에서 처리가 된다.
반면에, Process 과정과 Write과정에서 발생하는 데이터 선점에 대한 DeadlockLoserDataAccessException 등은 Retry를 통해 해결할 수 있다. 즉, 다른 프로세스에서 처리중인 데이터에 새로운 프로세스가 접근하는 경우 Lock이 걸려 있어 에러가 발생하는데 잠시 후 재시도 하면 성공할 수 있는 것이다.
✔ Read과정까지 성공한 데이터는 캐쉬에 저장된다. 그러므로 재시도가 일어날 경우 캐쉬의 데이터를 가져와 Process 과정부터 다시 수행한다.
✔ retryable exception은 기본적으로 rollback을 유발하므로 너무 많은 Retry는 성능을 저하시킬 수 있으므로 주의해야 한다.
다음 예시는 데이터를 처리하는 과정에서 <include> 로 설정된 DeadlockLoserDataAccessException 발생 시 Retry가 일어나도록 설정이 되어 있고, 이렇게 발생한 Retry는 3번까지만 허용한다. 그 이상의 Retry가 발생한다면 Step을 실패처리한다. 최초 데이터를 읽는 것부터 한번의 시도로 취급하므로, 예제에서는 두번의 시도를 더 할 수 있다.
<step id="step1">
<tasklet>
<chunk reader="itemReader" writer="itemWriter" commit-interval="2" retry-limit="3">
<retryable-exception-classes>
<include class="org.springframework.dao.DeadlockLoserDataAccessException"/>
</retryable-exception-classes>
</chunk>
</tasklet>
</step>
<job id="retryPolicyJob" xmlns="http://www.springframework.org/schema/batch">
<step id="retryPolicyStep">
<tasklet>
<chunk reader="reader" writer="writer" commit-interval="100" retry-policy="retryPolicy" />
</tasklet>
</step>
</job>
<bean id="retryPolicy" class="org.springframework.batch.retrypolicy.SimpleRetryPolicy">
<property name="maxAttempts" value="3" />
</bean>
<job id="job1" job-repository="jobRepository">
<step id="step1" parent="stepParent">
...
</step>
</job>
<bean id="stepParent" class="org.springframework.batch.core.step.item.FaultTolerantStepFactoryBean" abstract="true">
<property name="backOffPolicy">
<bean class="org.springframework.batch.retry.backoff.FixedBackOffPolicy"
<property name="backOffPolicy" value="2000" />
</bean>
</property>
</bean>
조금 더 견고하게 실패를 처리하고, 바로 이어서 시도해서 데이터 처리를 성공할 수 있다고 생각되는 경우, 자동으로 실패한 연산을 재시도하는 것이 도움이 된다. 예를 들어, 네트웍 문제로 실패한 웹 서비스나 ROM 서비스나 데이터베이스 갱신에서 발생한 DeadLockLoserException을 예로 들 수 있다. 스프링 배치에서는 이러한 연산을 자동으로 재시도 하기 위한 RestryOperations 전략을 갖고 있다.
public interface RetryOperations {
<T> T execute(RetryCallback<T> retryCallback) throws Exception;
<T> T execute(RetryCallback<T> retryCallback, RecoveryCallback<T> recoveryCallback)
throws Exception;
<T> T execute(RetryCallback<T> retryCallback, RetryState retryState)
throws Exception, ExhaustedRetryException;
<T> T execute(RetryCallback<T> retryCallback, RecoveryCallback<T> recoveryCallback,
RetryState retryState) throws Exception;
}
콜백은 재시도하는 비즈니스 로직을 넣을 수 있는 간단한 인터페이스다.
public interface RetryCallback<T> {
T doWithRetry(RetryContext context) throws Throwable;
}
콜백이 실행되고 예외가 발생해서 실패하는 경우, 성공할때까지 재시도 하게 된다. 또는 구현 여부에 따라 취소 여부를 결정한다.
가장 간단한 일반적으로 목적의 RetryOperations의 구현은 RetryTemplate이다.
RetryTemplate template = new RetryTemplate();
template.setRetryPolicy(new TimeoutRetryPolicy(30000L));
Foo result = template.execute(new RetryCallback<Foo>() {
public Foo doWithRetry(RetryContext context) {
// Do stuff that might fail, e.g. webservice operation
return result;
}
});
위 예제에서는 웹 서비스 호출을 실행하고 결과를 사용자에게 반환한다. 만약 호출이 실패하면 타임아웃이 될 때까지 재시도한다.
RetryCallback의 파라메터로 RetryContext가 있다. 콜백에서는 이 context를 무시하지만, 만약 필요하다면 반복되는 동안에 데이터를 저장하는 속성 가방(Attribute Bag)으로 사용할 수 있다.
Retry가 모두 사용되면, RetryOperation 은 다른 콜백(RecoveryCallback)에게 콜백 제어권을 넘길 수 있다. 이런 기능을 사용하려면 같은 방법으로 콜백에 전달하면 된다.
Foo foo = template.execute(new RetryCallback<Foo>() {
public Foo doWithRetry(RetryContext context) {
// business logic here
},
new RecoveryCallback<Foo>() {
Foo recover(RetryContext context) throws Exception {
// recover logic here
}
});
비즈니스로직이 템플릿 중단을 결정하기 전에 성공되지 못한다면, 클라이언트는 RecoveryCall을 통해 다른 처리를 할 기회가 주어진다.
단순한 Retry 방법은 RetryTemplate이 성공이나 실패를 할때까지 계속 시도하는 루프이다. RetryContext는 재시도 할건지 취소할 건지를 결정하는데 사용하는 상태를 포함하지만, 이 상태는 스택에 저장되고, 어디서나 접근하도록 글로벌하게 저장할 필요는 없다. 그러므로 우리는 이 방법을 무상태 재시도(Stateless Retry)라고 부른다. 무상태 재시도와 상태 유지 재시도 사이의 차이는RetryPolicy의 구현여부이다. (RetryTemplate은 둘 다 제어할 수 있다.) 무상태 재시도에서 Retry가 실패될 때, 콜백은 항상 동일한 쓰레드에서 수행된다.
트랜잭션 처리하는 자원을 무효화 시키는 실패는 몇가지 고려사항이 있다. (일반적으로)트랜잭션 처리가 없기 때문에 간단한 원격호출에 적용될 뿐만아니라 하이버네이트를 사용처럼 데이터베이스 갱신에도 적용된다. 이럴 경우 트랜잭션이 롤백되서, 다시 유효한 트랜잭션으로 시작할 수 있도록 실패가 되자마자 예외를 다시 던지는게 상황에 맞다.
예외를 다시 던지고(re-throw) 롤백하는 것은 남겨진 RetryOperations.execute메소드와 잠재적으로 스택에 있는 context를 손실하게 되기 때문에, 이런경우 무상태 재시도는 좋은 방법이 아니다. 이 손실을 피하기 위해서는 스택에서 context를 빼내서, 힙 저장 영역에 넣어두는 저장 전략을 도입해야 한다. 이러한 목적으로 스프링 배치는 RetryContextCache를 제공한다. RetryContextCache의 기본 구현은 단순하게 Map을 사용해서 메모리에 저장한다. (비록 클러스터 환경에서는 지나칠지라도) 클러스터 환경에서 다수의 프로세스를 처리하는 고급 사용법은 여러 종류의 클러스터 캐시를 사용하는 RetryContextCache 구현을 고려하자.
RetryOperations의 책임의 일부는 새로운 실행 (그리고 일반적으로 새 트랜잭션에 싸여)에 돌아 왔을 때 실패한 작업을 인식하는 것이다. 이 원할히 하기 위해, 스프링 배치는 RetryState 추상화를 제공한다. 실패한 작업을 인식하는 방법은 재시도의 여러 호출에 대한 상태를 식별하는 것이다. 상태를 확인하기 위해, 사용자는 Item을 식별하는 고유키를 리턴하는 RetryState객체를 제공해야 한다. 식별자는 RetryContextCache에서 키로 사용된다.
RertyState에 이해 리턴되는 키에서 Object.equals()와 Object.hashCode() 구현은 매우 조심해야 한다. 가장 추천하고 싶은 것은 Item을 구분할 수 있는 비즈니스키를 사용하는 것이다. JMS 메시지 경우에 messageID 가 사용될 수 있다.
RetryTemplate에서 execute 메소드의 재시도나 실패를 결정하는건 RetryPolicy에 의해서 결정된다. RetryPolicy는 RetryContext의 팩토리가 되기도 한다. RetryTemplate은 RetryContext를 만들기 위해서 현재 정책을 사용해야 할 책임을 갖으며, 시도마다 RetryCallback에 이를 전달한다. 콜백이 실패한 후에 RetryTemplate은 상태를 갱신하려고 RetryPolicy를 호출하며(RetryContext에 저장된다), 그 다음으로 또 다른 시도를 할 수 있는 경우에 RetryPolicy를 호출해서 정책을 문의하게 된다. (예를 들어, 제한에 걸렸거나 타입아웃이 되버린 것처럼) 또 다른 시도를 하지 못하게 되면, 정책은 다 사용된 상태를 관리하는 책임을 진다. 단순히 구현하자면 RetryExhastedException을 던지게 되고, 관련된 트랜잭션은 롤백된다. 좀 더 정교하게 구현하자면,트랜잭션을 손상하지 않고 유지할 수 있는 경우에 복구 행동을 시도할 수 있다.
실패는 태생적으로 재시도 여부가 결정된다. 일부 예외에서는 언제나 비지니스 로직 문제로 던져지므로 재시작 하는데 전혀 도움이 되지 못한다. 그러므로 모든 예외 타입에 대해서 재시도를 하면 안된다. 오로지 재시도를 할 수 있는 예외에만 집중해야 한다. 일반벅으로 더 공격적으로 재시도를 처리하는 것은 비즈니스 로직에 해가 되지는 않지만, 미리 실패를 알고 있는 것을 재시도하면서 시간을 소비하는 경우라면 비경제적이다.
스프링 배치는 범용목적의 statelses RetryPolicy의 구현을 제공한다. 예를 들어 SimpleRetryPolicy,TimeoutRetryPolicy 가 아래의 예에서 사용된다. SimpleRetryPolicy는 정해진 최대횟수만큼 예외유형의 리스트에 있는 재시도를 허락한다. SimpleRetryPolicy는 재시도되면 안되는 “치명적”예외 목록을 가지고 있으며, 재시도 동작 그 이상의 미세한 제어를 사용할 수 있도록 재시도가능한 목록에 덮어쓰기된다.
SimpleRetryPolicy policy = new SimpleRetryPolicy(5);
// Retry on all exceptions (this is the default)
policy.setRetryableExceptions(new Class[] {Exception.class});
// ... but never retry IllegalStateException
policy.setFatalExceptions(new Class[] {IllegalStateException.class});
// Use the policy...
RetryTemplate template = new RetryTemplate();
template.setRetryPolicy(policy);
template.execute(new RetryCallback<Foo>() {
public Foo doWithRetry(RetryContext context) {
// business logic here
}
});
사용자는 더 많은 재정의된 결정에 자신의 재시도 정책을 구현해야 할 수도 있다.
일시적인 실패 후에 재시도되 때, 실패의 원인이 되는 일부 문제들은 단지 잠시 기다리기만 해도 해결되는 경우가 있기 때문에, 많은 경우 다시 시도하기 전에 잠시 기다리는게 도움이 되기도 한다. RetryCallback이 실패한 경우,
public interface BackoffPolicy {
BackOffContext start(RetryContext context);
void backOff(BackOffContext backOffContext)
throws BackOffInterruptedException;
}
BackoffPolicy 는 자유롭게 방법을 선택해 구현하면 된다. 스프링 배치에서 제공되는 정책은 특별히 Object.waite()를 사용한다. 공통적인 사용은 두 재시도가 락에 걸리고 둘다 실패하는 것을 피하기 위해 긴 기다림이 증가하는 BackOff가 있다. 이러한 목적으로 스프링배치에서 ExponentialBackoffPolicy를 제공한다.
종종 서로 다른 다수의 반복에서 공통으로 걸리는하는 추가적인 콜백을 받아오는 것이 유용할 때가 있다. 이러한 목적으로 스프링 배치는 RetryListener 인터페이스를 제공한다. RetryTemplate은 사용자가 RetryListener를 등록하도록 해주며, 반복 동안에 이용할 수 있는 RetryContext와 Throwable 와 함께 콜백에 전해진다.
public interface RetryListener {
void open(RetryContext context, RetryCallback<T> callback);
void onError(RetryContext context, RetryCallback<T> callback, Throwable e);
void close(RetryContext context, RetryCallback<T> callback, Throwable e);
}
open과 close콜백이 모든 재시도 전후에 호출되며, onError()는 개별적인 RetryCallback 호출에 적용된다. 또한 close메소드는 RetryCallback에 의해서 마지막에 던저진 에러가 있는 경우에 Throwable을 받아올 수 있다. 다수의 리스너를 리스트로 갖으며 이는 순서가 있다. open메소드의 경우 동일한 순서로 호출되며,onError()와 close()는 역순으로 호출된다.
때때로 발생할 때마다 재시작하고 싶은 비즈니스 처리과정이 있다. 고전적인 예로 원격 서비스 호출이 있다. 스프링 배치는 이러한 목적에 딱 맞는 RetryOperations에서 호출되는 메소드를 감싸는 AOP 인터셉터를 제공한다.RetryOperationsInterceptor는 가로챈 메소드를 실행하고, 제공된 RetryTemplate에 있는 RetryPolicy에 따라서 실패를 재시도 한다.
아래는 remoteCall을 호출하는 메소드 서비스 호출을 재시도 하는데 스프링 AOP 네임스페이스를 사용하는 선언적인 재시도의 예제다.
<aop:config>
<aop:pointcut id="transactional" expression="execution(* com..*Service.remoteCall(..))" />
<aop:advisor pointcut-ref="transactional" advice-ref="retryAdvice" order="-1"/>
</aop:config>
<bean id="retryAdvice" class="org.springframework.batch.retry.interceptor.RetryOperationsInterceptor"/>
이 예에서는 인터셉터 내에 있는 기본 RetryTemplate을 사용한다. 리스너나 정책을 변경하기 위해 인터셉터에 RetryTemplate을 적용하는 것이 필요하다.
배치는 작업의 구성요소인 Step과 그 하위의 Chunk의 지속적인 반복수행으로 이루어진다. 여기서 반복수행은 Repeat정책을 따르며 구성요소별로 반복을 발생시킴으로써 배치를 수행하는 기능이다.
배치 처리 과정은 단순하게 최적화되거나 Job의 일부요소의 반복적인 행동이다. 스프링 배치는 반복을 전략적으로 일반화하고, iterator 프레임워크를 제공하기 위해 RepeatOperations 인터페이스를 가지고 있다.
RepeatOperations 인터페이스는 다음과 같다.
public interface RepeatOperations {
RepeatStatus iterate(RepeatCallback callback) throws RepeatException;
}
콜백은 반복되는 비즈니스 로직을 추가하도록 해주는 간단한 인터페이스다.
public interface RepeatCallback {
RepeatStatus doInIteration(RepeatContext context) throws Exception;
}
콜백은 반복이 끝났다고 결정될 때까지 반복적으로 실행된다. 이 인터페이스에서 반환하는 값은 RepeatStatus.CONTINUABLE 또는 RepeatStatus.FINISHED 이다. RepeatStatus는 더 수행할 작업이 있는지에 대한 repeat 수행 호출정보를 전달한다. 일반적으로 RepeatOperations의 구현은 RepeatStatus를 확인하고, 이것을 이용해 수행을 종료할지 반복할지에 대한 결정을 내린다. 호출자에게 더 이상 할 일이 없다는 신호를 보내고자 하는 모든 콜백은 ExistStatus.FINISHED를 반환하면 된다.
RepeatOperations의 가장 간단하고 일반적인 구현은 RepeatTemplate이다. 다음처럼 사용한다.
RepeatTemplate template = new RepeatTemplate();
template.setCompletionPolicy(new FixedChunkSizeCompletionPolicy(2));
template.iterate(new RepeatCallback() {
public ExitStatus doInIteration(RepeatContext context) {
// Do stuff in batch...
return ExitStatus.CONTINUABLE;
}
});
이 예에서는 계속 할 일이 있다는 것을 보여주는 ExitStatus.CONTINUABLE을 반환한다. 더 이상 수행할 것이 없다는 요청을 보내고 싶다면 ExitStatus.FINISHED 을 반환한다.
RepeatContext는 RepeatCallback의 메소드 인자다. 많은 콜백들은 단순하게 context를 무시하지만, 반복 하는 동안에 일시적으로 사용할 필요가 있는 데이터를 저장하는 속성 가방 (Attribute Bag)으로서 사용될 수 있다. iterate 메소드가 결과 를 반환한 후에, context는 더 이상 존재하지 않게 된다. RepeatContext는 처리 과정에서 내제된 반복이 필요한 경우 부모 context를 갖게 된다. 종종 부모 context는 반복되는 호출 사이에 공유할 필요가 있는 데이터를 저장하는데 유용하다.
ExitStatus는 스프링 배치에서 처리 과정이 끝났고, 처리가 성공인지 아닌지를 지정하는 목적으로 사용한다. 또는 배치나 반복의 종료 상태에 대한 정보(textual information)를 전달하는데 사용된다. 이 정보는 종료 코드의 형태와 자유로운 형식의 문자상태에 대한 설명이 된다.
| 프로퍼티 이름 | 설명 |
|---|---|
| CONTINUABLE | 작업이 남아 있음 |
| FINISHED | 더 이상의 반복 없음 |
RepeatStatus 값은 and 메소드를 사용해 논리 AND수행과 결합될 수 있다. 즉, 어떤 수행의 상태가 FINISHED 면 결과는 FINISHED 다.
RepeatTemplate 내에서 iterate 메소드에 있는 루프의 종료는 CompletionPolicy에 의해서 결정된다. CompletionPolicy 는 RepeatContext에 대한 팩토리도 된다. RepeatTemplate은 RepeatContext를 생성하는 정책을 이용해 반복 중 모든 단계에서 RepeatCallback에게 전달해야 하는 책임을 가지고 있다. 콜백이 완료된 후에 RepeatTemplate의 doInIteration는 상태를 갱신해야 하는지(RepeatContext에 저장될 것인지) 여부를 CompletionPolicy에게 물어보게 된다. 그 다음으로 반복이 완료된 경우에 정책을 요청하게 된다.
스프링 배치는 일반적인 목적으로 사용되는 간단한 CompletionPolicy 구현체를 제공한다. 위 예에서 사용한 SimpleCompletionPolicy을 예로 들 수 있다. SimpleCompletionPolicy는 고정된 시간만큼만 실행을 허용한다. (ExistStatus.FINISHED로 정해진 시간보다 강제적으로 일찍 완료할 수 있다.)
RepeatCallback 내에서 예외가 던져지는 경우, RepeatTemplate은 예외를 다시 던져야 하는지를 결정하는데 ExceptionHandler에게 의견을 묻게 된다.
public interface ExceptionHandler {
void handleException(RepeatContext context, Throwable throwable)
throws RuntimeException;
}
일반적인 사용방법은 주어진 타입의 예외발생 횟수를 세고, 한도에 도달했을때 실패한다. 이러한 목적에 맞게 스프링 배치는 SimpleLimitExceptionHandler와 조금 더 유연한 RethrowOnThresholdExceptionHandler를 제공한다. SimpleLimitExceptionHandler는 limit 프로퍼티와 현재 예외를 비교하는 예외 타입을 가지고 있다. 이 때 제공된 타입의 모든 하위 클래스들도 처리에 포함시킨다. 주어진 타입의 예외는 한계에 도달할 때까지는 무시되었다가 다시 던져지게 된다. 이러한 다른 예외 타입들도 항상 다시 던진다.
SimpleLimitExceptionHandler의 선택 가능한 중요한 프로퍼티는 useParent boolean 표시다. 기본값은 false기 때문에, 한계는 현재 RepeatContext에서만 설명된다. true로 설정되었을 때 한계는 내제된 반복(nested iteration)에서 형제context에 걸쳐 유지된다.
종종 서로 다른 다수의 반복에서 공통으로 걸리는 추가 콜백을 받아오는 것이 유용한 경우가 있다. 이러한 목적으로 스프링 배치는 RpeatListener 인터페이스를 제공한다. RepeatTemplate은 사용자가 RepeatListener를 등록할 수 있게 해준다. 그리고 콜백 반복 중에 이용할 수 있도록 RepeatContext와 RepeatStatus를 전달한다.
public interface RepeatListener {
void before(RepeatContext context);
void after(RepeatContext context, RepeatStatus result);
void open(RepeatContext context);
void onError(RepeatContext context, Throwable e);
void close(RepeatContext context);
}
open과 close 콜백은 개별적언 RepeatCallback 호출에 적용되어 before, after, onError 전후에 호출된다. 또한 다수의 리스너를 리스트로 갖으며 이는 순서가 있다. open과 before는 동일한 순서로 호출되며 after(), onError(), close()는 역순으로 호출된다.
RepeatOperations의 구현은 순차적으로 콜백을 실행하도록 제한하지 못한다. 구현은 동시에 콜백이 실행할 수 있도록 하는 건 제일 중요하다. 이 때문에 스프링 배치는 RepeatCallback을 실행하는데 TaskExecutor 전략을 사용하는 TaskExecutorRepeatTemplate을 제공한다. 기본적으로 (일반 RepeatTemplate과 같은) 동일한 쓰레드에 있는 전체반복을 수행하는 SynchronousTaskExecutor를 사용한다.
때때로 발생할 때마다 반복하고 싶은 비즈니스 처리과정이 있다. 고전적인 예제로 메세지 파이프라인의 최적화가 있다.메세지를 자주 받게 되는 경우, 매세지 마다 개별적인 트랜잭션으로 처리하는 비용을 참기 보다는 메세지를 배치로 처리 하는게 좀더 효율적이다. 스프링 배치는 이 목적에 맞게 RepeatOperations에서 메소드 호출을 감싸는 AOP 인터셉터를 제공한다. RepeatOperationsInterceptor는 가로챈 메소드를 실행해여 제공된 RepeatTemplate의 CompetionPolicy에 따라서 반복하게 된다.
여기서는 스프링 AOP 네임스페이스를 사용해서 호출되는 processMessage 메소드를 호출하는 서비스를 반복하는데 선언적인 반복의 예를 보자.
<aop:config>
<aop:pointcut id="transactional"
expression="execution(* com..*Service.processMessage(..))" />
<aop:advisor pointcut-ref="transactional"
advice-ref="retryAdvice" order="-1"/>
</aop:config>
<bean id="retryAdvice" class="org.spr...RepeatOperationsInterceptor"/>
이 예에서는 인터셉터 내에 있는 기본 RetryTemplate을 사용한다. 리스너나 정책을 변경하기 위해 인터셉터에 RetryTemplate을 적용하는 것이 필요하다.
가로챈 메소드가 void 반환 타입이라면, 인터셉터는 언제나 ExistStatus.CONTINUABLE을 반환한다. (그렇기 때문에 CompletionPolicy가 한정된 종료 지점이 없는 경우라면 무한 반복의 위험이 있다.) 만약 그렇지 않다면 가로챈 메소드에서 ExitStatus.FINISHED를 반환하는 지점이 되는 null을 반환할 때까지 ExitStatus.CONTINUABLE을 반환한다. 그래서 대상 메소드 내에 있는 비즈니스 로직은 null을 반환하거나 RepeatTemplate에서 제공하는 ExceptionHandler에 의해서 다시 던진 예외를 던져서 더 할 일이 없다는 신호를 보낼 수 있다.
Skip : http://static.springsource.org/spring-batch/reference/html/configureStep.html#configuringSkip
Retry : http://static.springsource.org/spring-batch/reference/html/configureStep.html#retryLogic http://static.springsource.org/spring-batch/reference/html/retry.html
Repeat : http://static.springsource.org/spring-batch/reference/html/repeat.html
배치 수행시 다수의 리소스를 처리하고자 할 경우에는 일반적인 Job설정으로 처리할 수 없다. 전자정부 배치프레임워크에서는 MultiData Processing을 통해 다수의 리소스를 읽어 다수의 결과로 처리하거나 다수의 리소스를 읽어 하나의 결과로 처리하는 기능을 제공한다.
다수(N개)의 리소스를 처리하는 방식은 N→1, N→N으로 구분된다.
두 방식을 개념적으로 비교하면 아래와 같다.

다수의 파일을 대상으로 동일한 유형의 Batch처리를 하고자 할 경우 MultiResourceItemReader를 사용하면 편리하다.
예를 들어, 아래와 같이 ‘file~‘로 시작하는 파일명을 가진 파일들에 대해 일괄 변경을 수행하고자 할 경우에도 적용 가능하다.
file-1.txt file-2.txt ignored.txt
Job수행에 사용되는 Reader및 Writer설정은 일반적인 Job과 동일하다.
<job id="multiResourceIoJob" xmlns="http://www.springframework.org/schema/batch">
<step id="multiResourceIoStep1">
<tasklet>
<chunk reader="itemReader" processor="itemProcessor" writer="itemWriter" commit-interval="2"/>
</tasklet>
</step>
</job>
MultiResourceItemReader를 통해 여러 개의 리소스를 읽어온 다음, 1개의 리소스를 처리하는 Reader에게 데이터처리를 위임한다.
이 때, input resource경로에 *를 사용하여 다수의 파일을 처리 가능하다.
<bean id="itemReader" class="org.springframework.batch.item.file.MultiResourceItemReader" scope="step">
<property name="delegate">
<bean class="org.springframework.batch.item.file.FlatFileItemReader">
<property name="lineMapper">
<bean class="org.springframework.batch.item.file.mapping.DefaultLineMapper">
<property name="lineTokenizer">
<bean class="org.springframework.batch.item.file.transform.DelimitedLineTokenizer">
<property name="delimiter" value="," />
<property name="names" value="name,credit" />
</bean>
</property>
<property name="fieldSetMapper">
<bean class="org.springframework.batch.item.file.mapping.BeanWrapperFieldSetMapper">
<property name="targetType"
value="egovframework.brte.sample.common.domain.trade.CustomerCredit" />
</bean>
</property>
</bean>
</property>
</bean>
</property>
<property name="resources" value="classpath:data/input/file-*.txt" />
</bean>
MultiResourceItemWriter를 통해 출력파일의 개수를 지정한 다음, 1개의 리소스를 처리하는 Writer에게 데이터처리를 위임한다.
<bean id="itemWriter" class="org.springframework.batch.item.file.MultiResourceItemWriter" scope="step">
<property name="resource" value="#{jobParameters['output.file.path']}" />
<property name="itemCountLimitPerResource" value="6" />
<property name="delegate" ref="delegateWriter" />
</bean>
<bean id="delegateWriter" class="org.springframework.batch.item.file.FlatFileItemWriter">
<property name="lineAggregator">
<bean class="org.springframework.batch.item.file.transform.DelimitedLineAggregator">
<property name="delimiter" value="," />
<property name="fieldExtractor">
<bean class="org.springframework.batch.item.file.transform.BeanWrapperFieldExtractor">
<property name="names" value="name,credit" />
</bean>
</property>
</bean>
</property>
</bean>
스프링 배치에서는 Composite처리와 관련하여 CompositeWriter만을 제공하고 있다. 이에 전자정부 배치프레임워크에서는 CompositeReader를 추가적으로 제공한다.
CompositeReader의 일반적인 처리 프로세스는 아래와 같다.

✔ 주의! CompositeReader는 등록된 모든 Reader로부터 데이터를 한 라인씩 순서대로 읽어와서 배열에 넣어주는 역할까지 수행한다.따라서,Writer를 바로 사용하면 안되고 Processor에서 배열을 읽어서 처리하는 과정이 반드시 필요하다.
CompositeReader에는 Reader에서 리소스를 처리해서 Processor로 VO를 전달하는 유형과 Reader자체를 그대로 Processor로 전달하는 유형으로 나뉘며,
Processor에서는 전달된 데이터 타입에 맞게 비즈니스 로직을 구현할 수 있다.
| 유형 I - VO(ValueObject)를 Processor에 전달하는 유형 | 유형 II - Reader를 Processor에 전달하는 유형 |
|---|---|
![]() | ![]() |
<bean id="compositeItemReader"
class="egovframework.brte.core.item.composite.reader.EgovCompositeFileReader">
<property name="itemsMapper">
<bean
class="egovframework.brte.core.item.composite.EgovCompositeItemMapper" />
</property>
<property name="returnType" value="vo" />
<property name="itemReaderList">
<list>
<ref bean="itemReader1" />
<ref bean="itemReader2" />
</list>
</property>
</bean>
``` |
```xml
<bean id="compositeItemReader"
class="egovframework.brte.core.item.composite.reader.EgovCompositeFileReader">
<property name="itemsMapper">
<bean
class="egovframework.brte.core.item.composite.EgovCompositeItemMapper" />
</property>
<property name="returnType" value="reader" />
<property name="itemReaderList">
<list>
<ref bean="itemReader1" />
<ref bean="itemReader2" />
</list>
</property>
</bean>
``` |
#### Job 설정
CompositeItem처리를 위한 Job설정은 reader에 compositeItemReader를 설정하고, processor를 지정해야 한다.
```xml
<job id="compositeItemJob" xmlns="http://www.springframework.org/schema/batch">
<step id="compositeItemStep">
<tasklet>
<chunk reader="compositeItemReader" processor="itemProcessor" writer="itemWriter"
commit-interval="5" />
</tasklet>
</step>
</job>
ComposteItemReader를 설정하기 위해서는 Reader로 사용할 Class, returnType, itemReaderList 항목을 작성해야 한다.
<bean id="compositeItemReader"
class="egovframework.brte.core.item.composite.reader.EgovCompositeFileReader">
<property name="itemsMapper">
<bean
class="egovframework.brte.core.item.composite.EgovCompositeItemMapper" />
</property>
<property name="returnType" value="vo" />
<property name="itemReaderList">
<list>
<ref bean="itemReader1" />
<ref bean="itemReader2" />
</list>
</property>
</bean>
<bean id="itemReader1" class="org.springframework.batch.item.file.FlatFileItemReader" scope="step">
<property name="resource" value="#{jobParameters[inputFile]}" />
<property name="lineMapper">
<bean class="org.springframework.batch.item.file.mapping.DefaultLineMapper">
<property name="lineTokenizer">
<bean class="org.springframework.batch.item.file.transform.FixedLengthTokenizer">
<property name="names" value="name,credit" />
<property name="columns" value="1-9,10-11" />
</bean>
</property>
<property name="fieldSetMapper">
<bean class="org.springframework.batch.item.file.mapping.BeanWrapperFieldSetMapper">
<property name="targetType" value="egovframework.brte.sample.common.domain.trade.CustomerCredit" />
</bean>
</property>
</bean>
</property>
</bean>
<bean id="itemReader2" class="org.springframework.batch.item.file.FlatFileItemReader" scope="step">
<property name="lineMapper">
<bean class="org.springframework.batch.item.file.mapping.DefaultLineMapper">
<property name="lineTokenizer">
<bean class="org.springframework.batch.item.file.transform.DelimitedLineTokenizer">
<property name="delimiter" value=","/>
<property name="names" value="name,credit" />
</bean>
</property>
<property name="fieldSetMapper">
<bean class="org.springframework.batch.item.file.mapping.BeanWrapperFieldSetMapper">
<property name="targetType" value="egovframework.brte.sample.common.domain.trade.CustomerCredit" />
</bean>
</property>
</bean>
</property>
<property name="resource" value="#{jobParameters[inputFile]}" />
</bean>
✔ CompositeItemReader에서 사용가능한 Class는 총 3개가 존재하며 각 Class별 특징은 다음과 같다.
| 종류 | 설명 |
|---|---|
| EgovCompositeFileReader | FlatFile(FixedLength,Delimited), XMLFile 등 파일처리를 위한 용도로 사용 |
| EgovCompositeCursorReader | JdbcCursorItemReader를 통해 DB리소스 처리를 하고자 할 경우에 사용 |
| EgovCompositePagingReader | JdbcPagingItemReader, IbatisPagingItemReader 등 Paging단위로 DB리소스 처리를 하고자 할 경우에 사용 |
CompositeItemReader를 통해 넘어온 VO 또는 Reader를 Processor에서 꺼내와서 처리하는 로직을 구현해야 한다.
public class FileItemProcessor implements ItemProcessor<EgovCompositeDataProvider, TargetVO> {
public TargetVO process(EgovCompositeDataProvider eprovider) throws Exception {
Object[] obj = eprovider.getMapItems();
//배열에서 항목을 꺼내오는 로직
CustomerCredit vo1 = (CustomerCredit)obj[0];
CustomerCredit vo2 = (CustomerCredit)obj[1];
//비즈니스 로직 처리 가능
TargetVO vo = new TargetVO();
if(vo1 !=null) {
vo.setId(vo1.getId());
}
if(vo2 != null) {
vo.setName(vo2.getName());
}
return vo;
}
}
✔ 주의!
배치작업 처리 중의 정보는 JobRepository의 JobInstance, JobParams, JobExecution, StepExecution, key-value 쌍으로 값을 보관할 수 있는 공간인 ExecutionContext에 저장 및 갱신되어 history를 관리한다.
JobInstance, JobParams, JobExecution, StepExecution, ExecutionContext 의 각각의 속성에 대해서 정리하였다.
| JobInstance 속성 | 설명 |
|---|---|
| jobInstanceId | JobInstance를 식별하는 ID |
| version | JobInstance 의 수정 횟수 |
| jobName | Job의 이름 |
| jobKey | JobInstance를 구분 짓는 JobParameters의 serialization |
| JobExecution 속성 | 설명 |
|---|---|
| status | BatchStatus는 실행 상태를 나타내는 객체이다, 실행하는 동안에는 BatchStatus,STARTED, 실행이 실패한 경우 BatchStatus.FAILED, 실행이 성공적으로 종료됐을 경우 BatchStatus.COMPLETED가 된다. |
| startTime | Execution이 시작되는 현재 시스템 시간을 java.Util.Data로 저장 |
| endTime | Execution의 성공/실패 여부와 관계없이 종료되는 현재 시스템 시간을 java.Util.Data로 저장 |
| exitStatus | ExitStatus는 실행의 결과를 나타낸다. 호출자에게 반환될 exit code를 포함한다. |
| createTime | JobExecution이 최초 생성 된 현재 시스템 시간을 java.Util.Data로 저장 |
| lastUpdated | JobExecution이 마지막으로 생성 된 현재 시스템 시간을 java.Util.Data로 저장 |
| executionContext | execution간 지속돼야 할 모든 데이터를 포함하는 ‘프로퍼티 백’ |
| failureExceptions | Job이 실행되는 동안 발생한 익셉션 리스트 |
| JobParams 속성 | 설명 |
|---|---|
| jobInstanceId | BATCH_JOB_INSTANCE 테이블의 jobInstanceId를 외래키로 지정 |
| typeCd | 파라마터의 형식을 String으로 저장,null일 될 수 없음 |
| keyName | 파라미터의 키 |
| stringVal | String타입의 파마미터 값 |
| dateVal | Date타입의 파마미터 값 |
| longVal | Long타입의 파마미터 값 |
| doubleVal | Double타입의 파마미터의 값 |
| StepExecution 속성 | 설명 |
|---|---|
| status | BatchStatus는 실행 상태를 나타내는 객체이다, 실행하는 동안에는 BatchStatus,STARTED, 실행이 실패한 경우 BatchStatus.FAILED, 실행이 성공적으로 종료됐을 경우 BatchStatus.COMPLETED가 된다. |
| startTime | Execution이 시작되는 현재 시스템 시간을 java.Util.Data로 저장 |
| endTime | Execution의 성공/실패 여부와 관계없이 종료되는 현재 시스템 시간을 java.Util.Data로 저장 |
| exitStatus | ExitStatus는 실행의 결과를 나타낸다. 호출자에게 반환될 exit code를 포함한다. |
| executionContext | execution간 지속돼야 할 모든 데이터를 포함하는 ‘프로퍼티 백’ |
| readCount | 성공적으로 읽은 item 갯수 |
| writeCount | 성공적으로 쓰인 item 갯수 |
| commitCount | 해당 execution에서 커밋된 트랜젝션 횟수 |
| rollbackCount | 롤백된 Step에 의해서 제어된 비즈니스 트랜젝션의 갯수 |
| readSkipCount | 읽기 과정에서 실패 후, 스킵된 item 갯수 |
| processSkipCount | 프로세스 과정에서 실패 후, 스킵된 item 갯수 |
| filterCount | ItemProcessor에 의해 필터링 된 item 갯수 |
| writeSkipCount | 쓰기 과정에서 실패 후, 스킵된 item 갯수 |
| JobExecutionContext 속성 | 설명 |
|---|---|
| jobExecutionId | BATCH_JOB_EXECUTION 테이블의 jobExecutionId를 외래키로 지정 |
| shortContext | SERIALIZED_CONTEXT의 문자열 버전 |
| serializedContext | 전체 Context |
| StepExecutionContext 속성 | 설명 |
|---|---|
| stepExecutionId | BATCH_STEP_EXECUTION 테이블의 stepExecutionId를 외래키로 지정 |
| shortContext | SERIALIZED_CONTEXT의 문자열 버전 |
| serializedContext | 전체 Context |
일괄(배치)처리 작업 수행 시, 작업처리가 종료될 때까지 대기하는 동기방식 처리와 작업처리의 종료를 Callback매커니즘을 이용하여 전달받는 비동기처리에 대한 예제를 보여준다.
Job 수행시, 동기와 비동기 방식으로 데이터를 처리할 수 있으며, 이 예제에서는 동기 처리가 기본값으로 설정되어 있다. 설정위치는 Launcher 설정파일의 jobLauncher 빈에서 taskExecutor 프로퍼티이며, 참조하는 값으로 다음 두 가지를 설정할 수 있다.
<bean id="jobLauncher" class="org.springframework.batch.core.launch.support.SimpleJobLauncher">
<property name="jobRepository" ref="jobRepository" />
<property name="taskExecutor" ref="sync"/> <!-- 비동기시 ref="async" -->
</bean>
<!-- 동기 처리시 sync -->
<bean id="sync" class="org.springframework.core.task.SyncTaskExecutor" />
<!-- 비동기 처리시 async -->
<bean id="async" class="org.springframework.core.task.SimpleAsyncTaskExecutor" />
동기/비동기 처리 예제를 위해 특별히 Job을 설정하는 내용은 없다. 이 예제에서 제공하는 Job의 상세 내용은 기존 업무 재사용 예제의 Job 설정과 같으므로 이를 참고한다.
<job id="delegateJob" xmlns="http://www.springframework.org/schema/batch">
<step id="delegateStep1">
<tasklet>
<chunk reader="reader" writer="writer" commit-interval="3"/>
</tasklet>
</step>
</job>
<bean id="reader" class="org.springframework.batch.item.adapter.ItemReaderAdapter">
<property name="targetObject" ref="delegateObject" />
<property name="targetMethod" value="getData" />
</bean>
<bean id="writer" class="org.springframework.batch.item.adapter.PropertyExtractingDelegatingItemWriter">
<property name="targetObject" ref="delegateObject" />
<property name="targetMethod" value="processPerson" />
<property name="fieldsUsedAsTargetMethodArguments">
<list>
<value>firstName</value>
<value>address.city</value>
</list>
</property>
</bean>
<bean id="delegateObject" class="egovframework.brte.sample.common.domain.person.PersonService" />
Item Processor를 비동기 처리하기 위해 Spring Batch에서 AsyncItemProcessor 서비를 지원한다.
AsyncItemProcessor 서비스를 이용한 설정은 아래와 같다.
<task:executor id="taskExecutor" pool-size="100"/>
<bean id="itemProcessorAsync" class="org.springframework.batch.integration.async.AsyncItemProcessor">
<!-- delegate통해 실제 동작 할 Item Processor를 설정한다. -->
<property name="delegate" ref="fixedLengthToFixedLengthJob.fixedLengthToFixedLengthStep.itemProcessor"/>
<!-- Executor를 설정한다. -->
<property name="taskExecutor" ref="taskExecutor" />
</bean>
✔ JunitTest 클래스의 구조는 배치실행환경 예제 Junit Test 설명을 참고한다.
✔ assertEquals(“COMPLETED”, jobExecution.getExitStatus().getExitCode()) : 배치수행결과가 COMPLETED 인지 확인한다.
✔ Thread.sleep(4000) : 비동기로 배치를 수행 시, DB에 배치상태(UNKNOWN)를 셋팅하고 DB연결이 종료되어 Job이 정상적으로 수행되더라도 종료상태(COMPLETED,FAILED)를 확인할 수 없다. 예제에서는 Job결과를 확인하기 위해 Thread를 적정시간동안 정지시켜 인위적으로 종료상태를 확인하도록 설정하였다.
@ContextConfiguration(locations = { "/egovframework/batch/sync-job-launcher-context.xml",
"/egovframework/batch/jobs/delegatingJob.xml",
"/egovframework/batch/job-runner-context.xml" })
public class EgovSyncDelegatingJobFunctionalTests {
...
@Test
public void testLaunchJob() throws Exception {
JobExecution jobExecution=null;
try{
jobExecution =jobLauncherTestUtils.launchJob();
//Async 로 수행되는 경우 Exit Status는 UNKNOWN으로 설정 됨
assertEquals("UNKNOWN", jobExecution.getExitStatus().getExitCode());
Thread.sleep(4000);
}catch (InterruptedException ie){
ie.printStackTrace();
}
assertTrue(personService.getReturnedCount() > 0);
assertEquals(personService.getReturnedCount(), personService.getReceivedCount()) ;
assertEquals("COMPLETED", jobExecution.getExitStatus().getExitCode());
}
}
수행방법은 JunitTest 실행을 참고한다.
배치 수행시 Job을 구성하는 각 단계(Job, Step, Chunk, Read, Process, Write)에서 이벤트설정을 통해 다양한 추가구성을 할 수 있다. 이벤트는 Listener를 활용하여 설정하고, 배치 수행중 설정한 Listener를 접하게 되면 관련된 이벤트를 수행하게 된다.
Job 실행 과정에서, 사용자가 정의한 코드가 실행 될 수 있도록 Job의 라이프사이클에서 다양한 이벤트로 알려주는 것은 유용하다.SimpleJob는 적절한 시간에 JobListener를 호출하도록 한다.
public interface JobExecutionListener {
void beforeJob(JobExecution jobExecution);
void afterJob(JobExecution jobExecution);
}
JobListeners는 Job의 리스너들을 통해 SimpleJob에 추가 될 수 있다.
<job id="footballJob">
<step id="playerload" parent="s1" next="gameLoad"/>
<step id="gameLoad" parent="s2" next="playerSummarization"/>
<step id="playerSummarization" parent="s3"/>
<listeners>
<listener ref="sampleListener"/>
</listeners>
</job>
Job의 성공, 실패에 관계없이 afterJob이 호출되어야한다. 만약 성공과 실패의 결정이 필요하다면 JobExecution에서 얻을 수 있다.
public void afterJob(JobExecution jobExecution){
if( jobExecution.getStatus() == BatchStatus.COMPLETED ){
//job success
}
else if(jobExecution.getStatus() == BatchStatus.FAILED){
//job failure
}
}
✔ JobExecutionListener 인터페이스에 해당하는 Annotations
| Annotations | 설명 |
|---|---|
| @BeforeJob | Job수행 전에 호출 |
| @AfterJob | Job수행 후에 호출 |
StepExecutionListener는 Step 실행에서 가장 일반적인 리스너이다. Step에 대한 정보를 Step 시작전과 종료후에 알려준다.
public interface StepExecutionListener extends StepListener {
void beforeStep(StepExecution stepExecution);
ExitStatus afterStep(StepExecution stepExecution);
}
afterStep의 반환 리턴타입인 ExitStatus는 청리스너에게 단계 완료시 반환되는 exit-code를 수정 할 수있는 기회를 준다.
이 인터페이스에 해당하는 주석입니다
✔ StepExecutionListener 인터페이스에 해당하는 Annotations
| Annotations | 설명 |
|---|---|
| @BeforeStep | Step 수행 전에 호출 |
| @AfterStep | Step 수행 후에 호출 |
Chunk는 트랜잭션 범위 내에서 아이템을 처리하는 것이다. 트랙잭션을 커밋하고, commit interval 단위로 Chunk를 커밋한다. ChunkListener는 Chunk 처리를 시작하기 전에 또는 성공적으로 완료 한 후 구현로직을 수행한다.
public interface ChunkListener extends StepListener {
void beforeChunk();
void afterChunk();
}
beforeChunk 메소드는 트랜잭션이 시작해서 ItemReader의 read 수행되기전에 불린다. 반대로, afterChunk 메소드는 rollback이 일어나지 않고 Chunk가 커밋된 후에 불린다.
✔ ChunkListener 인터페이스에 해당하는 Annotations
| Annotations | 설명 |
|---|---|
| @BeforeChunk | Chunk 수행 전에 호출 |
| @AfterChunk | Chunk 수행 후에 호출 |
✔ TaskletStep 수행같은 Chunk 선언이 없는 경우에는 ChunListener을 적용할 수 없다.
Skip 로직 설명에서, 나중에 처리 할 수 있도록 Skip로그를 기록할 수 있다고 언급했다. read 오류의 경우, ItemReaderListener 가 이 작업을 수행한다.
public interface ItemReadListener<T> extends StepListener {
void beforeRead();
void afterRead(T item);
void onReadError(Exception ex);
}
beforeRead 메소드는 ItemReader에서 read 수행 전에 호출된다.
afterRead 메소드는 read 수행이 성공했을때 호출되고, 읽은 item을 전달한다.
Read 수행중 오류가 발생하는 경우 onReadError 메서드가 호출된다. 기록을 위해 발생한 예외정보가 전달된다.
✔ ItemReadListener인터페이스에 해당하는 Annotations
| Annotations | 설명 |
|---|---|
| @BeforeRead | Read수행 전에 호출 |
| @AfterRead | Read수행 후에 호출 |
| @OnReadError | Read 수행중 오류발생시 호출 |
ItemReadListener 처럼, item 처리에서도 리스너가 있다.
public interface ItemProcessListener<T, S> extends StepListener {
void beforeProcess(T item);
void afterProcess(T item, S result);
void onProcessError(T item, Exception e);
}
beforeProcess 메소드는 ItemProcessor의 process 과정 이전에 호출이 되고, 처리되는 item 을 전달한다.
afterProcess 메소드는 item 이 성공적으로 처리된 후에 호출된다.
만약 처리과정에서 에러가 발생한다면, onProcessError가 호출되는데 예외정보와 item을 전달하므로 기록을 남길 수 있다.
✔ ItemProcessListener 인터페이스에 해당하는 Annotations
| Annotations | 설명 |
|---|---|
| @BeforeProcess | Process 수행 전에 호출 |
| @AfterProcess | Process 수행 후에 호출 |
| @OnProcessError | Process 수행중 오류발생시 호출 |
ItemWriteListener로 item을 쓰는 과정에서 리스너를 호출할 수 있다.
public interface ItemWriteListener<S> extends StepListener {
void beforeWrite(List<? extends S> items);
void afterWrite(List<? extends S> items);
void onWriteError(Exception exception, List<? extends S> items);
}
beforeWrite 메소드는 ItemWriter에서 write 수행 전에 호출되고 쓰여진 item을 전달한다.
afterWrite 메소드는 write 수행이 성공했을때 호출되고, 읽은 아이템을 전달한다.
write 수행중 오류가 발생하는 경우 onWriteError 메소드가 호출된다. 기록을 위해 발생한 예외정보와 item들이 리스트형식으로 전달된다.
✔ ItemWriteListener 인터페이스에 해당하는 Annotations
| Annotations | 설명 |
|---|---|
| @BeforeWrite | Write 수행 전에 호출 |
| @AfterWrite | Write 수행 후에 호출 |
| @OnWriteError | Write 수행중 오류 발생시 호출 |
itemReadListener, ItemProcessListener, ItemWriteListner 에서 에러발생을 알려주는 메카니즘을 제공하지만, item 처리가 Skip 될 경우에는 아무도 알려주지 않는다. 이를 위해 Skip된 item을 추적하는 인터페이스가 있다.
public interface SkipListener<T,S> extends StepListener {
void onSkipInRead(Throwable t);
void onSkipInProcess(T item, Throwable t);
void onSkipInWrite(S item, Throwable t);
}
onSkipInRead 메소드는 read 수행중 Skip 발생할경우 언제나 호출된다. 주의할 점은 한 번 이상 Skip 되었기에 롤백은 동일한 항목이 등록될 수 있다는 것을 알려야 한다.
onSkipInWrite 메소드는 write 수행중 Skip 이 발생한 경우 호출되는데, 여기서 해당 item은 성공적으로 read 되었기 때문에 item 그 자체를 인수로 제공한다.
✔ SkipListener 인터페이스에 해당하는 Annotations
| Annotations | 설명 |
|---|---|
| @OnSkipInRead | Read 수행중 Skip 발생시 호출 |
| @OnSkipInWrite | Write 수행중 Skip 발생시 호출 |
| @OnSkipInProcess | Process 수행중 Skip 발생시 호출 |
SkipListener에 대한 가장 일반적인 사용 사례 중 하나는 다른 일괄 처리나 사람이 직접 처리할 때 사용할 수 있도록 Skip된 item을 기록하는 것이다.
✔ 트랜잭션에서 롤백 될 수 있는 경우가 많기 때문에 스프링에서는 다음 두가지를 보장한다.
1. 적절한 Skip 방법은(오류에 따라) 항목 당 한 번만 호출된다.
2. SkipListener는 항상 트랜잭션 커밋 전에 호출된다. 그러므로 리스너에 의해 호츨된 트랜잭션 자원은 ItemWriter에서 실패하여 롤백되지 않는다.
public class EventNoticeListener {
@Autowired
EgovEmailEventNoticeTrigger egovEmailEventNoticeTrigger;
// Job 수행완료 후 수행
@AfterJob
public ExitStatus sendJobNotice(JobExecution jobExecution) {
egovEmailEventNoticeTrigger.invoke(jobExecution);
...
}
// Step 수행완료 후 수행
@AfterStep
public ExitStatus sendStepNotice(StepExecution stepExecution) {
egovEmailEventNoticeTrigger.invoke(stepExecution);
...
}
// Read 중 Error 발생시 수행
@OnReadError
public void sendErrorNotice(Exception e) {
egovEmailEventNoticeTrigger.invoke(e);
...
}
}
전자정부 표준프레임워크에서는 스프링에서 제공하는 다양한 Listener를 배치작업의 구성요소(Job, Step, Chunk)별로 나누고 각 단계의 전/후로 나누어, 클래스 이름만으로 독립적인 역할을 명확히 알 수 있는 Processor를 제공한다. Processor 들은 Job 설정파일의 <listener>가 호출하며, 각각의 Processor가 호출되는 위치는 다음 그림을 참조한다.
✔ 그림의 EgovSampleXXXProcessor는 예시 클래스이며, 전자정부 표준프레임워크에서 제공하는 Processor를 상속받아 구현된 클래스이다.

| 클래스명 | 제공메소드명 | 파라미터 | 설명 |
|---|---|---|---|
| EgovJobPreProcessor | beforeJob() | JobExecution | Job 단계 이전에 호출 |
| EgovJobPostProcessor | afterJob() | JobExecution | Job 단계 이후에 호출 |
public class EgovJobPreProcessor extends JobExecutionListenerSupport {
/**
* Job 수행 이전에 호출되는 부분
*/
public void beforeJob(JobExecution jobExecution) {
}
}
public class EgovJobPostProcessor extends JobExecutionListenerSupport {
/**
* Job 수행 이후에 호출되는 부분
*/
public void afterJob(JobExecution jobExecution) {
}
}
위 클래스를 상속받아 사용자가 정의한 Job Processor는 설정파일에서 <listeners>를 이용해 다음과 같은 위치에 설정한다.
<job id="ProcessorJob" xmlns="http://www.springframework.org/schema/batch">
<listeners>
<listener ref="jobListener" />
</listeners>
<step id="ProcessorStep">
<tasklet>
<chunk reader="itemReader" writer="itemWriter" commit-interval="2"/>
</tasklet>
</step>
</job>
<bean id="jobListener" class="사용자가 정의한 Job Processor 클래스" />
| 클래스명 | 제공메소드명 | 파라미터 | 설명 |
|---|---|---|---|
| EgovStepPreProcessor | beforeStep() | StepExecution | Step 단계 이전에 호출 |
| EgovStepPostProcessor | afterStep() | StepExecution | Step 단계 이후에 호출 |
public class EgovStepPreProcessor<T, S> extends StepListenerSupport<T, S> {
/**
* Step 수행 이전에 호출되는 부분
*/
public void beforeStep(StepExecution stepExecution) {
}
}
public class EgovStepPostProcessor<T, S> extends StepListenerSupport<T, S> {
/**
* Step 수행 이후에 호출되는 부분
*/
public ExitStatus afterStep(StepExecution stepExecution) {
return null;
}
}
위 클래스를 상속받아 사용자가 정의한 Step Processor는 설정파일에서 <listeners> 를 이용해 다음과 같은 위치에 설정한다.
<job id="ProcessorJob" xmlns="http://www.springframework.org/schema/batch">
<step id="ProcessorStep">
<tasklet>
<chunk reader="itemReader" writer="itemWriter" commit-interval="2"/>
</tasklet>
<listeners>
<listener ref="stepListener" />
</listeners>
</step>
</job>
<bean id="stepListener" class="사용자가 정의한 Step Processor 클래스" />
| 클래스명 | 제공메소드명 | 파라미터 | 설명 |
|---|---|---|---|
| EgovChunkPreProcessor | beforeChunk() | 없음 | Chunk 단계 이전에 호출 |
| EgovChunkPostProcessor | afterChunk() | 없음 | Chunk 단계 이후에 호출 |
public class EgovChunkPreProcessor extends ChunkListenerSupport {
/**
* Chunk 수행 이전에 호출되는 부분
*/
public void beforeChunk() {
}
}
public class EgovChunkPostProcessor extends ChunkListenerSupport {
/**
* Chunk 수행 이후에 호출되는 부분
*/
public void afterChunk() {
}
}
위 클래스를 상속받아 사용자가 정의한 Chunk Processor는 설정파일에서 <listeners> 를 이용해 다음과 같은 위치에 설정한다.
<job id="ProcessorJob" xmlns="http://www.springframework.org/schema/batch">
<step id="ProcessorStep">
<tasklet>
<chunk reader="itemReader" writer="itemWriter" commit-interval="2">
<listeners>
<listener ref="chunkListener" />
</listeners>
</chunk>
</tasklet>
</step>
</job>
<bean id="chunkListener" class="사용자가 정의한 Chunk Processor 클래스" />
http://static.springsource.org/spring-batch/reference/html/configureJob.html#interceptingJobExecution
http://static.springsource.org/spring-batch/reference/html/configureStep.html#interceptingStepExecution
대용량 데이터를 처리하는 배치수행에서 병렬처리를 이용하면, Job의 구성요소들이 여러 쓰레드 분산수행되어 빠른 시간 내에 효율적으로 작업을 완료할 수 있다. 스프링 배치에서 병렬처리 방식은 실행 유형별로 멀티쓰레드 방식, Parallel 방식, 파티셔닝 방식 등이 있다.
병렬처리를 시작하는 간단한 방법은 Step 구성요소중 <tasklet> 속성에 TaskExecutor를 추가하는 것이다.
<step id="loading">
<tasklet task-executor="taskExecutor">...</tasklet>
</step>
TaskExecutor 예제에서 TaskExecutor 인터페이스를 구현하기 위해 빈을 정의한다. TaskExecutor 는 스프링 인터페이스 표준이므로 상세한 내용은 스프링 가이드를 참고한다. 가장 간단한 멀티쓰레드 TaskExecutor 는 SimpleAsyncTaskExecutor 이다. 위 Step 구성으로 수행한 결과 각 청크단위의 reading, processing, writing 과정이 분리된 쓰레드에서 수행된다. 즉, 처리시 순서를 보장하지 않으며 Chunk는 단일 쓰레드 수행과 비교해 item 들이 연속적이지 않다.(commit-Interval의 영향으로 Chunk 내의 순서는 같을 수 있다.)
✔ 쓰레드 수는 기본값으로 4가 설정되어 있으나, 필요하다면 다음처럼 증가시켜 사용한다.
<step id="loading">
<tasklet task-executor="taskExecutor" throttle-limit="20">
...
</tasklet>
</step>
✔ DataSource 처럼 Step에서 사용되는 풀이 리소스들에 의해 대체될 수 있다. 그러므로 Step에서 병행처리 되는 쓰레드 수를 원하는 만큼 최대한 풀을 설정해야 한다.
병행처리가 필요한 응용프로그램 로직은 서로 다른 책임으로 분할될 뿐만 아니라, 각 단계에서 할당되면 그것이 한 프로세스에서 병행처리가 될 수 있다. Parallel Step 수행은 사용하고 구성하기 쉽다. 예를 들어, step3 와 병행처리할 스텝들(step1, step2)은 다음처럼 흐름을 설정하면 된다.
<job id="job1">
<split id="split1" task-executor="taskExecutor" next="step4">
<flow>
<step id="step1" parent="s1" next="step2"/>
<step id="step2" parent="s2"/>
</flow>
<flow>
<step id="step3" parent="s3"/>
</flow>
</split>
<step id="step4" parent="s4"/>
</job>
<beans:bean id="taskExecutor" class="org.spr...SimpleAsyncTaskExecutor"/>
task-executor 속성은 각각의 흐름을 실행하는데 필요한 TaskExecutor 구현을 지정하기 위해 사용한다. 기본 설정은 SyncTaskExecutor 이고, 비동기로 실행하려면 이 설정을 AsyncTaskExecutor 로 변경해 주어야 한다.
✔ 각 작업 분할은 최종 종료 상태로 통합되기 전에 모두 완료하도록 구성해야 한다. 아래 그림처럼 분리된 flow들이 모두 완료해야만 다음 step으로 진행가능하다.

스프링배치는 Step의 파티셔닝 수행을 원격으로 파티셔닝하기 위하여 SPI를 제공한다. 다음 수행 패턴을 그림으로 표현했다.

Job은 왼쪽부터 Step의 흐름대로 진행된다. Step들 중에서 하나는 마스터 라벨로 지정되어 있다. 이 그림에서 슬레이브 라벨들은 Step의 인스턴스로 식별되고, 그 결과가 마스터의 결과로 귀속된다. 슬레이브는 전형적으로 원격서비스로 이루어지며 로칼쓰레드로 전달된다. JobRepository 의 스프링배치 meta-data에서 각각의 슬레이브가 Job에서 각각 한번 수행되는 것을 보장한다.
스프링배치의 SPI는 Step의 특별한 구현(PartitionStep)으로 구성되어있다. PartitionHandler와 StepExecutionSplitter 라는 두개의 인터페이스가 있고 이 역할은 아래 그림을 참고한다.

오른쪽의 Step은 잠재적으로 여러 객체를 갖고 있고 각 역할을 수행하는 remote 슬레이브이다. Partition Step은 다음과 같이 구성되어 있다.
<step id="step1.master">
<partition step="step1" partitioner="partitioner">
<handler grid-size="10" task-executor="taskExecutor"/>
</partition>
</step>
✔ 멀티쓰레드 스텝의 throttle-limit 속성과 유사하게 grid-size 속성이 있어서, 이것이 각 Step의 요청이 포화상태가 되는 것을 방지한다.
PartitionHandler 는 Remote 환경이나 Grid 환경의 구조를 알고있는 컴포넌트이다. 이것은 DTO 같은 포맷으로 감싸 StepExecution을 원격에 있는 Step에 보낼 수 있다. 여기에서는 입력데이터들이 어떻게 나누어지는지, 멀티 Step 수행결과들이 어떻게 합쳐지는지 알 필요가 없다. 하지만 스프링 배치는 TaskExecutor 전략을 이용하여 분리된 쓰레드에서 Step들을 수행시키는 유용한 PartitionHandler 도 제공한다. 이런 구현체를 TaskExecutorPartitionHandler라고 하는데 XML 로 구성된 Step에 기본값으로 정해져 있다. 또한, 다음처럼 구성할 수 있다.
<step id="step1.master">
<partition step="step1" handler="handler"/>
</step>
<bean class="org.spr...TaskExecutorPartitionHandler">
<property name="taskExecutor" ref="taskExecutor"/>
<property name="step" ref="step1" />
<property name="gridSize" value="10" />
</bean>
✔ gridSize는 새로 생성할 분리된 Step의 수를 의미하는데, TaskExecutor에서 쓰레드풀 갯수와 같다. gridSize는 사용가능한 쓰레드 수 이상으로 설정이 될 수 있는데, 이 경우 작은 수가 적용이 된다.
Partitioner는 새로운 Step Execution을 위한 입력 파라미터와 같은 유사한 역할을 한다.(재시작에 대해 걱정할 필요가 없다.) 아래와 같은 인터페이스가 있으며 메소드는 partition 메소드 하나뿐이다.
public interface Partitioner {
Map<String, ExecutionContext> partition(int gridSize);
}
여기서 메소드의 리턴값은 ExecutionContext 타입과 String 타입의 유일한 이름인 stepExecution이다. 여기서 이름은 파티션된 StepExecution의 Step 이름으로 이후에 배치의 meta data에서 보여준다. ExecutionContext 는 이름, 값으로 구성된 여러 쌍들이 담긴 가방이며 기본키, line 수, 입력 파일의 위치등의 정렬을 포함할 수도 있다.
StepExecution 들의 이름은 Job의 StepExecution이 서로 달라야 한다. 가장좋은 방법은 prefix+suffix 의 형태로 사용자를 위한 의미있는 이름을 만드는 것이다. 여기서 prefix는 실행되는 Step의 이름이고, suffix는 카운터이다. SimplePartitioner가 이와 같은 형태의 StepExecution 이름을 지정한다.
Step이 동일한 구성을 갖는 것은 PartitionHandler에 의해 실행하는 Step과 ExecutionContext에서 런타임에 바인딩하기 위한 입력 매개 변수에 매우 효율적이다. 이것은 스프링 배치의 기능인 Step Scope기능과 비슷하다.
예를 들어, Partitioner 가 fileName 키 속성을 갖고 있는 ExecutionContext 인스턴스를 생성한다면, 각 Step들은 서로다른 파일들을 바라보게 되고 출력결과는 다음처럼 다.
| Step Execution Name (key) | ExecutionContext (value) |
|---|---|
| filecopy:partition0 | fileName=/home/data/one |
| filecopy:partition1 | fileName=/home/data/two |
| filecopy:partition2 | fileName=/home/data/three |
배치 처리시 Code 기반으로 에러를 처리 할 수 있도록 EgovBatchException를 통해서 지원한다. 데이터베이스 에러코드관리 테이블을 등록과 에러코드 데이터를 등록이 선행 되야지만 해당 서비스를 사용가능하다.
C REATE TABLE BATCH_EXCEPTION_MESSAGE (
EX_ID BIGINT NOT NULL PRIMARY KEY,
EX_KEY VARCHAR(255) NOT NULL,
EX_MESSAGE VARCHAR(2500) NOT NULL
);
I NSERT INTO BATCH_EXCEPTION_MESSAGE VALUES(1,'EGOVBATCH000001','배치실행 중 업무 관련 에러가 발생 하였습니다.');
I NSERT INTO BATCH_EXCEPTION_MESSAGE VALUES(2,'EGOVBATCH000002','배치실행 중 알수 없는 오류가 발생 하였습니다.');
에러처리 생성자 생성자 파리미터 데이터베이스소스, 에러코드를 사용하여 에러처리를 할 수 있습니다.
try{
...
}catch(Exception e){
throw new EgovBatchException(dataSource,"EGOVBATCH000001");
//Sql 설정시 EgovBatchException 생성자 파리미터 추가
//throw new EgovBatchException(dataSource,"EGOVBATCH000001","SELECT EX_MESSAGE FROM BATCH_EXCEPTION_MESSAGE WHERE EX_KEY = ?");
}
전자정부 표준프레임워크에서의 큐(Queue)를 사용하여 대용량 데이터 처리를 위해 센터 컷 방식의 배치 작업수행을 위한 가이드를 제공한다.
Unordered List Item기본적으로 센터컷의 구조는 큐(Queue)를 이용하는 부분을 제외하고는 배치 프로그램과 유사하다.
Unordered List Item처음 ItemReader를 사용하여 데이터를 읽고 큐에 넣은 Center-Cut Reading Step과, 읽어온 데이터를 가공 후 QueueSender를 통해 Queue에 넣는 구조이다.

Center-Cut Process Step은 큐에서 들어온 데이터를 읽고 처리 모듈(Busineess Proc)를 활용하여 데이터를 처리하는 구조이다.

[참고] QueueSender, QueueReciever만 센터컷을 위해 추가되는 모듈을 가이드하며, 나머지는 배치와 동일하다.
Queue를 사용한 센터 컷 배치 처리를 위하여 Apache ActiveMQ를 활용한다. 현재 가이드의 ActiveMQ의 버전은 apache-activemq-5.15.1이며, 관련 프로그램 및 활용 방법은 아래의 ActiveMQ에서 확인할 수 있다. Apache ActiveMQ ActiveMQ를 설치 및 실행 후, http:localhost:8161에서 실행 확인을 할 수 있으며, 관리자 계정(admin/admin) 로그인 후 Queue 메뉴에서 현재 큐의 사황을 확인 할 수 있다.

구성 된 ActiveMQ를 활용하여 1개의 배치 Job과 2개의 Step(QueueSender, QueueProc)를 처리하는 가이드 예제를 활용한다.
가이드 예제의 기본 구성
<job id="centerCutJob" parent="eGovBaseJob" xmlns="http://www.springframework.org/schema/batch">
<step id="stepQueueSender" next="stepQueueProc">
<tasklet ref="taskletQueueSender" />
</step>
<step id="stepQueueProc">
<tasklet ref="taskletQueueProc" />
</step>
</job>
<bean id="taskletQueueSender" class="egovframework.rte.bat.centercut.TaskletQueueSender" scope="step">
</bean>
<bean id="taskletQueueProc" class="egovframework.rte.bat.centercut.TaskletQueueProc" scope="step">
</bean>
TaskletQueueSender를 통하여 10,000개의 임의 데이터를 전송하고 endSender를 통하여 end message를 전송한다. (총 전송되는 메시지는 10,000개 메시지와 1개의 end message를 전송)
public RepeatStatus execute(StepContribution contribution,
ChunkContext chunkContext) throws Exception {
LOGGER.debug("TaskletQueueSender execute START ===");
QueueSenderFactory qf = new QueueSenderFactory("test_queue");
TextMessage txMessage = qf.getMessage();
MessageProducer sender = qf.getSender();
qf.setRemove(true);
for(int i=0; i<10000; i++){
LOGGER.debug("Send Value : " + i );
txMessage.setText(String.valueOf(i));
sender.send(txMessage);
senderCount++;
}
qf.endSender(sender);
// * 오류로 주석처리
qf.close();
// LOGGER를 사용하는 것과 달리 MessageFormatter를 사용할 경우 Log 레벨과 상관 없이 결과를 로그에 기록함.
// 따라서 개발/운영계에 로그레벨이 달라지는 경우에도 출력을 보장함
LOGGER.debug("########## Center-Cut Result ##########");
LOGGER.debug("## Sender Count : " + senderCount);
LOGGER.debug("########################################");
return RepeatStatus.FINISHED;
}
public void endSender(MessageProducer sender) throws Exception {
TextMessage message = session.createTextMessage();
setRemove(true);
message.setText("End Of QUEUE"); //end message
sender.send(message);
}
xxxx-xx-xx 11:20:16,566 DEBUG [egovframework.rte.bat.centercut.TaskletQueueSender] Send Value : 9989
xxxx-xx-xx 11:20:16,566 DEBUG [egovframework.rte.bat.centercut.TaskletQueueSender] Send Value : 9990
xxxx-xx-xx 11:20:16,566 DEBUG [egovframework.rte.bat.centercut.TaskletQueueSender] Send Value : 9991
xxxx-xx-xx 11:20:16,566 DEBUG [egovframework.rte.bat.centercut.TaskletQueueSender] Send Value : 9992
xxxx-xx-xx 11:20:16,566 DEBUG [egovframework.rte.bat.centercut.TaskletQueueSender] Send Value : 9993
xxxx-xx-xx 11:20:16,566 DEBUG [egovframework.rte.bat.centercut.TaskletQueueSender] Send Value : 9994
xxxx-xx-xx 11:20:16,566 DEBUG [egovframework.rte.bat.centercut.TaskletQueueSender] Send Value : 9995
xxxx-xx-xx 11:20:16,566 DEBUG [egovframework.rte.bat.centercut.TaskletQueueSender] Send Value : 9996
xxxx-xx-xx 11:20:16,567 DEBUG [egovframework.rte.bat.centercut.TaskletQueueSender] Send Value : 9997
xxxx-xx-xx 11:20:16,567 DEBUG [egovframework.rte.bat.centercut.TaskletQueueSender] Send Value : 9998
xxxx-xx-xx 11:20:16,567 DEBUG [egovframework.rte.bat.centercut.TaskletQueueSender] Send Value : 9999
xxxx-xx-xx 11:20:16,570 DEBUG [org.apache.activemq.transport.tcp.TcpTransport] Stopping transport tcp:///127.0.0.1:61616
xxxx-xx-xx 11:20:26,574 DEBUG [egovframework.rte.bat.queue.QueueRemover] >>>>> amqJmxUrl = service:jmx:rmi:///jndi/rmi://localhost:1099/jmxrmi
xxxx-xx-xx 11:20:26,725 DEBUG [egovframework.rte.bat.queue.QueueRemover] >>>>> init MBeanServerConnection connection = javax.management.remote.rmi.RMIConnector$RemoteMBeanServerConnection@3bcd426c
xxxx-xx-xx 11:20:26,725 DEBUG [egovframework.rte.bat.queue.QueueRemover] >>>>> queue remove - clientServiceName = org.apache.activemq:type=Broker,brokerName=localhost
xxxx-xx-xx 11:20:26,728 DEBUG [egovframework.rte.bat.queue.QueueRemover] >>>>> ConsumerCount = 0
xxxx-xx-xx 11:20:26,728 DEBUG [egovframework.rte.bat.queue.QueueRemover] >>>>> queue remove - clientServiceName = org.apache.activemq:type=Broker,brokerName=localhost
xxxx-xx-xx 11:20:26,728 DEBUG [egovframework.rte.bat.queue.QueueRemover] >>>>> queue remove - queueName = test_queue
xxxx-xx-xx 11:20:26,728 DEBUG [egovframework.rte.bat.queue.QueueRemover] >>>>> queue remove - operationName = removeQueue
xxxx-xx-xx 11:20:26,738 DEBUG [egovframework.rte.bat.centercut.TaskletQueueSender] ########## Center-Cut Result ##########
xxxx-xx-xx 11:20:26,738 DEBUG [egovframework.rte.bat.centercut.TaskletQueueSender] ## Sender Count : 10000
xxxx-xx-xx 11:20:26,738 DEBUG [egovframework.rte.bat.centercut.TaskletQueueSender] ########################################
또한, 전자정부 표준프레임워크는 TaskletQueueProc를 통하여 10,000개의 데이터를 전송받아 처리하면서, end message를 통하여 배치 처리를 종료한다. (총 10,000개 메시지를 처리하고 1개의 end message를 통하여 완료한다.)
public RepeatStatus execute(StepContribution contribution,
ChunkContext chunkContext) throws Exception {
LOGGER.debug("TaskletQueueProc execute START ===");
QueueReceiverFactory qf = new QueueReceiverFactory("test_queue");
MessageConsumer receiver = qf.getReceiver();
LOGGER.debug("=====>>>>> Start");
while (true){
textMessage = (TextMessage)receiver.receive();
if(textMessage.getText().equals("End Of QUEUE")){
LOGGER.debug("**********Receive End Message: " + textMessage.getText());
qf.sessionCommit();
break;
}
LOGGER.debug("Receive Message: " + textMessage.getText());
qf.sessionCommit();
recieveCount++;
}
qf.close();
LOGGER.debug("########## Center-Cut Result ##########");
LOGGER.debug("## Recieve Count : " + recieveCount);
LOGGER.debug("########################################");
return RepeatStatus.FINISHED;
}
xxxx-xx-xx 11:45:27,042 DEBUG [egovframework.rte.bat.centercut.TaskletQueueProc] Receive Message: 9997
xxxx-xx-xx 11:45:27,042 DEBUG [org.apache.activemq.ActiveMQSession] ID:DESKTOP-NV1780L-60402-1518144303940-3:1:1 Transaction Commit :TX:ID:DESKTOP-NV1780L-60402-1518144303940-3:1:9998
xxxx-xx-xx 11:45:27,042 DEBUG [org.apache.activemq.TransactionContext] Commit: TX:ID:DESKTOP-NV1780L-60402-1518144303940-3:1:9998 syncCount: 1
xxxx-xx-xx 11:45:27,043 DEBUG [org.apache.activemq.TransactionContext] Begin:TX:ID:DESKTOP-NV1780L-60402-1518144303940-3:1:9999
xxxx-xx-xx 11:45:27,043 DEBUG [egovframework.rte.bat.centercut.TaskletQueueProc] Receive Message: 9998
xxxx-xx-xx 11:45:27,043 DEBUG [org.apache.activemq.ActiveMQSession] ID:DESKTOP-NV1780L-60402-1518144303940-3:1:1 Transaction Commit :TX:ID:DESKTOP-NV1780L-60402-1518144303940-3:1:9999
xxxx-xx-xx 11:45:27,043 DEBUG [org.apache.activemq.TransactionContext] Commit: TX:ID:DESKTOP-NV1780L-60402-1518144303940-3:1:9999 syncCount: 1
xxxx-xx-xx 11:45:27,044 DEBUG [org.apache.activemq.TransactionContext] Begin:TX:ID:DESKTOP-NV1780L-60402-1518144303940-3:1:10000
xxxx-xx-xx 11:45:27,044 DEBUG [egovframework.rte.bat.centercut.TaskletQueueProc] Receive Message: 9999
xxxx-xx-xx 11:45:27,044 DEBUG [org.apache.activemq.ActiveMQSession] ID:DESKTOP-NV1780L-60402-1518144303940-3:1:1 Transaction Commit :TX:ID:DESKTOP-NV1780L-60402-1518144303940-3:1:10000
xxxx-xx-xx 11:45:27,044 DEBUG [org.apache.activemq.TransactionContext] Commit: TX:ID:DESKTOP-NV1780L-60402-1518144303940-3:1:10000 syncCount: 1
xxxx-xx-xx 11:45:27,045 DEBUG [org.apache.activemq.TransactionContext] Begin:TX:ID:DESKTOP-NV1780L-60402-1518144303940-3:1:10001
xxxx-xx-xx 11:45:27,045 DEBUG [egovframework.rte.bat.centercut.TaskletQueueProc] **********Receive End Message: End Of QUEUE
xxxx-xx-xx 11:45:27,045 DEBUG [org.apache.activemq.ActiveMQSession] ID:DESKTOP-NV1780L-60402-1518144303940-3:1:1 Transaction Commit :TX:ID:DESKTOP-NV1780L-60402-1518144303940-3:1:10001
xxxx-xx-xx 11:45:27,045 DEBUG [org.apache.activemq.TransactionContext] Commit: TX:ID:DESKTOP-NV1780L-60402-1518144303940-3:1:10001 syncCount: 1
xxxx-xx-xx 11:45:27,050 DEBUG [org.apache.activemq.ActiveMQMessageConsumer] remove: ID:DESKTOP-NV1780L-60402-1518144303940-3:1:1:1, lastDeliveredSequenceId:60066
xxxx-xx-xx 11:45:27,052 DEBUG [org.apache.activemq.transport.tcp.TcpTransport] Stopping transport tcp:///127.0.0.1:61616
xxxx-xx-xx 11:45:27,052 DEBUG [egovframework.rte.bat.centercut.TaskletQueueProc] ########## Center-Cut Result ##########
xxxx-xx-xx 11:45:27,052 DEBUG [egovframework.rte.bat.centercut.TaskletQueueProc] ## Recieve Count : 10000
xxxx-xx-xx 11:45:27,052 DEBUG [egovframework.rte.bat.centercut.TaskletQueueProc] ########################################
전자정부 표준프레임워크의 배치 수행중 특정시점에 이벤트가 발생하는 경우 EventNoticeTrigger 인터페이스를 활용하여 SMS, Email 등을 통해 정보를 전달할 수 있는 추상화된 외부알림 access 관리기능이다.
EventNoticeTrigger 는 이벤트 알림 기능을 사용하도록 추상화된 인터페이스이다.
메소드는 트리거를 호출하는 invoke 메소드를 파라메터 타입별로 구성하였고, 모든 메소드의 리턴타입은 void 이다.
| 메소드 | 파라메터 | 설명 |
|---|---|---|
| invoke() | 없음 | 파라메터 없이 이벤트호출 로직 구현 |
| invoke(StepExecution) | StepExecution | 파라메터인 StepExecution 정보를 활용하는 로직 구현 |
| invoke(JobExecution) | JobExecution | 파라메터인 JobExecution 정보를 활용하는 로직 구현 |
| invoke(Exception) | Exception | 파라메터인 Execption 정보를 활용하는 로직 구현 |
EgovEventNoticeTrigger 클래스는 EventNoticeTrigger 인터페이스를 상속받은 형태로, 파라메터 타입별로 이벤트 알림 기능(Email전송, SMS전송)을 호출하기 위한 invoke 메소드를 갖고 있다. 모든 메소드의 리턴타입은 void로 로직수행 후 반환해야 하는 값이 없고, 이는 배치 고유의 수행과정과 별도로 동작하는 부분이다.
사용자는 EgovEventNoticeTrigger 를 상속받아 각각의 파라메터 타입별로 재정의하여 사용한다.

‘프로세서(리스너) 설정’ 부분과 ‘트리거 설정’ 부분으로 나뉜다.
<job id="eventNoticeTriggerJob" xmlns="http://www.springframework.org/schema/batch">
<step id="eventNoticeTriggerStep1">
<tasklet>
<chunk reader="itemReader" writer="itemWriter" commit-interval="2" />
</tasklet>
<listeners>
<listener ref="EventNoticeCallListener" />
</listeners>
</step>
</job>
<bean id="EventNoticeCallListener" class="egovframework.brte.sample.example.listener.EgovEventNoticeCallProcessor" />
✔ 이때 사용자가 정의한 클래스는 Job 설정파일에 빈으로 등록되어 있으므로 어노테이션을 활용한다
public class EgovEventNoticeCallProcessor<T,S> extends EgovStepPostProcessor<T,S> {
//EgovEventNoticeTrigger을 상속받아 재정의한 클래스
@Autowired
EgovEmailEventNoticeTrigger egovEmailEventNoticeTrigger;
public ExitStatus afterStep(StepExecution stepExecution) {
...
egovEmailEventNoticeTrigger.invoke(stepExecution);
...
}
}
아래 예시에서는 StepExecution 파라메터를 활용하였다.
✔ 이때 사용자가 정의한 클래스는 설정파일에서 빈으로 등록되어 있다.
public class EgovEmailEventNoticeTrigger extends EgovEventNoticeTrigger {
public void invoke(StepExecution stepExecution) {
// StepExecution 을 활용하여 알림내용 작성
// '트리거 설정' 1에서 구현한 트리거를 통해 메시지 전송 수행
}
}
<-- 프로세서(리스너) 설정 1 의 Job 설정파일 (계속) -->
<bean id="EmailEventNoticeTrigger"
class="egovframework.brte.sample.example.event.EgovEmailEventNoticeTrigger" />
위 기능을 활용하여 실제 Email을 전송하는 예제를 제공하므로 아래를 참고한다.
Job 내부에는 여러 Step 들이 존재할 수 있고, 각 Step 사이의 흐름을 관리할 필요가 있다. Step 내의 next 설정과 Desision 설정으로 Job을 수행하다 한 Step의 처리결과에 따라 다른 Step을 선택하여 수행할 수 있고, 특정 Step의 실패가 Job 전체의 실패로 이어지지 않도록 구성할 수 있다.
가장 간단한 시나리오의 Job은 모든 Step을 순서대로 실행 하는 것이다.

위와 같은 Job의 실행은 Step 엘리먼트의 ’next’ 어트리뷰트를 이용해서 설정할 수 있다.
<job id="job">
<step id="stepA" parent="s1" next="stepB" />
<step id="stepB" parent="s2" next="stepC"/>
<step id="stepC" parent="s3" />
</job>
위 시나리오를 실행 하면 Job 설정의 가장 상단에 위치한 ‘stepA’가 먼저 실행 된다. ‘stepA’가 실행 완료 되면 ‘stepB’, ‘stepC’의 순서로 실행되게 된다. 하지만 만약 ‘stepA’의 실행이 실패하게 된다면 전체 Job의 실행은 실패하게 되며 ‘stepB’는 실행 되지 않는다.
위 Sequential Flow의 실행 결과는 다음 2가지로 나뉘게 된다.
많은 케이스가 위의 내용에 해당하게 된다. 하지만 Step의 실패가 Job의 실패를 유발하는 것이 아니고 다른 Step을 실행하는 경우를 생각해 보자.

스프링 배치에서는 다양한 경우의 시나라오를 설정 할 수 있도록 Step 엘리먼트내에 사용할 수 있는 transition 엘리먼트를 제공 해준다. 그중의 한 엘리먼트로 ’next’ 엘리먼트가 있다. ’next’ 엘리먼트는 ’next’ 어트리뷰트와 마찬가지로 Job 실행에서 다음에 실행될 Step에 대해 알려준다. 하지만 ’next’ 어트리뷰트와 다르게 ’next’ 엘리먼트는 사용 횟수에 제한이 없으며, Step의 실패에 대한 default 설정이 없다. 그러므로 transition 엘리먼트를 사용할 때에는 모든 Step의 Behavior 설정을 충분히 해줘야 한다.
✔ 싱글 Step의 경우 ’next’ 어트리뷰트와 transition 엘리먼트를 사용할 수 없다.
다음은 ’next’ 엘리먼트를 사용하는 기본 패턴이다
<job id="job">
<step id="stepA" parent="s1">
<next on="*" to="stepB" />
<next on="FAILED" to="stepC" />
</step>
<step id="stepB" parent="s2" next="stepC" />
<step id="stepC" parent="s3" />
</job>
’next’ 엘리먼트의 ‘on’ 어트리뷰트를 이용해서 Step 실행 결과(ExitStatus)에 따라 다음 Step을 설정 할 수 있다.‘on’ 어트리뷰트에 패턴으로 사용 될 수 있는 특수 문자는 다음 2가지만이 가능하다.
예를 들면 ‘c*t’는 ‘cat’과 ‘count’와 모두 매칭 된다. 반면에, ‘c?t’는 ‘cat’과 매칭 되지만 ‘count’와 매칭 되지 않는다. Step 설정에 transition 엘리먼트의 사용 횟수에는 제한이 없지만, Step의 실행 결과(ExitStatus)가 Step 설정에 정의 되지 않은 경우에는 스프링 배치 프레임워크는 예외를 던지게 되며 Job의 실행은 실패하게 된다. 스프링 배치 프레임워크는 ‘on’ 어트리뷰트 설정 값을 상세한 설정을 우선해서 먼저 적용한다. 위 설정을 예로 들면 Step의 실행 결과로 ‘FAILED’ ExitStatus를 갖게 되면 ‘StepA’가 아닌 ‘StepC’로 전이 하게 된다.
Conditional flow를 사용하는 Job을 설정할때에는 BatchStatus과 ExitStatus의 차이를 아는것이 중요하다. BatchStatus는 Job 또는 Step 의 실행 결과를 스프링 프레임워크에서 기록할 때 사용하는 Property의 집합니다. BatchStatus로 사용 되는 값은 COMPLETED, STARTING, STARTED, STOPPING, STOPPED, FAILED, ABANDONED, UNKNOWN 이다. 대부분의 값들은 단어와 같은 뜻으로 해석하여 이해하면 된다.
다음의 ’next’ 엘리먼트를 사용하는 예제를 보자.
<next on="FAILED" to="stepB" />
위 예제에서 ‘on’ 어트리뷰트가 BatchStatus를 참조하는 것으로 생각되기 쉽지만 실제 참조되는 값은 Step의 ExitStatus이다. ExitStatus의 이름에서 알 수 있듯이 ExitStatus는 Step의 실행 후 상태를 알려주는 값이다. 위 예제를 좀더 쉽게 풀이 하자면 ’exit 코드가 FAILED로 끝나게 되면 StepB로 가라’는 뜻이 된다. 스프링 배치 프레임워크는 디폴트 설정으로 ExitStatus의 exit 코드는 Step의 BatchStatus와 같도록 설정이 되어 있다. 하지만 만약에 exit 코드가 BatchStatus와 달라야 한다면
<step id="step1" parent="s1">
<end on="FAILED" />
<next on="COMPLETED WITH SKIPS" to="errorPrint1" />
<next on="*" to="step2" />
</step>
위 Step의 실행 결과는 다음 3가지가 될 수 있다.
위 예제의 사용에는 에러가 없지만, 사용자의 의도대로 처리되기 위해서는’COMPLETED WITH SKIPS’ exit 코드를 반환하는 별도의 로직이 필요하다.
public class SkipCheckingListener extends StepExecutionListenerSupport {
public ExitStatus afterStep(StepExecution stepExecution) {
String exitCode = stepExecution.getExitStatus().getExitCode();
if (!exitCode.equals(ExitStatus.FAILED.getExitCode()) &&
stepExecution.getSkipCount() > 0) {
return new ExitStatus("COMPLETED WITH SKIPS");
}
else {
return null;
}
}
}
위코드를 설명하면 StepExecutionListener 에서는 먼저 Step이 성공적으로 수행되었는지 체크한다. 그 후 StepExecution의 skip 횟수가 0.1f보다 클경우 ‘COMPLETED WITH SKIPS’의 exit 코드를 갖는 ExitStatus를 반환한다.
BatchStatus와 ExitStatus가 어떻게 결정되는지 알아 보았다. Step의 Status는 코드의 실행 결과에 의해 결정 되는 반면에 Job의 Status는 스프링 배치 설정에 의해 결정 되어 진다. 모든 Job 설정은 최소 하나의 ’transition이 없는 final Step’ 을 갖게 된다. 다음의 예는 Step이 실행 되어 진 후에 Job이 종료 되게 된다.
<step id="stepC" parent="s3"/>
transition이 정의 되지 않은 Step의 경우 Job의 Status는 다음과 같이 결정 되게 된다.
단순 Sequential Step의 Job의 경우에는 위 방법만으로도 Job을 설정하기에 충분하지만, 별도의 Job의 중단을 위한 시나리오가 필요한 경우도 존재한다. 이같은 경우를 위해 스프링 배치 프레임워크에서는 Job을 중단시키기 위한 transition 엘리먼트를 제공한다. 이 엘리먼트들은 Job을 특정 BatchStatus와 함께 종료 시킨다.
Stop transition 엘리먼트는 Step의 BatchStatus 또는 ExitStatus에는 영향을 미칠수 없으며, 오로지 Job의 마지막 상태에만 영향을 미칠수 있다. 예를 들어, Job의 모든 Step의 결과가 FAILED이지만 Job의 Status는 COMPLETED가 될 수 있으며 ,그반대 또한 가능하다.
’end’ 엘리먼트는 Job을 COMPLETED BatchStatus와 함께 종료 시킨다. COMPLETED 상태로 종료된 Job은 재실행이 불가능하다(스프링 배치 프레임워크에서 JobInstanceAlreadyCompleteException이 발생됨). ’end’ 엘리먼트에는 추가적으로 ’exit-code’ 어트리뷰트를 사용하면 커스텀 ExitStatus의 정의가 가능하다. ’exit-code’ 정의가 없는 경우에는 디폴트 코드로 COMPLETED가 적용되게 된다.
다음의 예제는 Step2가 실패하기 되면 Job은 COMPLETED BatchStatus로 종료되며 Step3은 실행 되지않는다. 이경우 Job이 COMPLETED BatchStatus로 종료 되었기 때문에 재시작이 불가능 하다.(Step2가 성공하는 경우에는 Step3이 실행되게 된다)
<step id="step1" parent="s1" next="step2">
<step id="step2" parent="s2">
<end on="FAILED"/>
<next on="*" to="step3"/>
</step>
<step id="step3" parent="s3">
‘fail’ 엘리먼트는 Job을 FAILED BatchStatus와 함께 종료 시킨다. ’end’엘리먼트와 다르게 ‘fail’ 엘리먼트를 사용한 경우에는 Job의 재시작이 가능하다. ‘fail’ 엘리먼트 또한 커스텀 ExitStatus의 정의를 위한 ’exit-code’ 어트리뷰트를 사용 할 수 있다. ’exit-code’ 정의가 없는 경우에는 디폴트 코드로 FAILED가 적용되게 된다.
다음의 예제는 Step2가 실패하기 되면 Job은 FAILED BatchStatus와 EARLY TERMINATION ExitStatus로 종료되며 Step3은 실행 되지 않는다. 이경우 Job의 재시작이 가능하며, 실행은 Step2부터 시작된다.(Step2가 성공하는 경우에는 Step3이 실행되게 된다)
<step id="step1" parent="s1" next="step2">
<step id="step2" parent="s2">
<fail on="FAILED" exit-code="EARLY TERMINATION"/>
<next on="*" to="step3"/>
</step>
<step id="step3" parent="s3">
‘stop’ 엘리먼트는 Job을 STOPPED BatchStatus와 함께 종료 시킨다. Job이 중단 되면 Job이 재시작 되기전에 오퍼레이터는 추가적인 작업을 수행 할 수 있다. ‘stop’ 엘리먼트는 다음 Job의 재실행 지점의 설정을 위해 ‘restart’ 어트리뷰트의 정의가 요구된다.
다음의 예제에서는 Step1이 COMPLETED 상태로 종료가 되면 Job은 정지 되게 되며, 정지된 Job의 재실행 지점은 Step2이
<step id="step1" parent="s1">
<stop on="COMPLETED" restart="step2"/>
</step>
<step id="step2" parent="s2"/>
경우에 따라서는 다음에 실행될 Step을 결정하기 위해 ExitStatus 보다 더 많은 정보가 필요할 경우가 있다. 이때에 다음 Step을 결정하기 위해 JobExecutionDecider를 이용 할 수 있다.
public class MyDecider implements JobExecutionDecider {
public FlowExecutionStatus decide(JobExecution jobExecution, StepExecution stepExecution)
{
if (someCondition) {
return "FAILED";
}
else {
return "COMPLETED";
}
}
}
Job 설정을 위한 ‘decision’ 태그를 다음과 같이 사용한다.
<job id="job">
<step id="step1" parent="s1" next="decision" />
<decision id="decision" decider="decider">
<next on="FAILED" to="step2" />
<next on="COMPLETED" to="step3" />
</decision>
<step id="step2" parent="s2" next="step3"/>
<step id="step3" parent="s3" />
</job>
<beans:bean id="decider" class="com.MyDecider"/>
앞에서 설명한 모드 시나리에서는 Job내의 Step들의 실행이 순서대로 이루어지고 있다. 스프링 배치프레임워크에서는 이외에도 parallel flow를 지원하기 위한 ‘split’ 엘리먼트를 제공한다.
아래의 코드를 보면 ‘split’ 엘리먼트는 하나 이상의 ‘flow’ 엘리먼트를 갖을 수 있으며, ‘flow’ 엘리먼트는 분리된 flow를 정의한다. ‘split’ 엘리먼트는 앞에서 설명한 ’next’ 어트리뷰트, ’next’ 엘리먼트, ’end’ 엘리먼트, ‘fail’ 엘리먼트, ‘pause’ 엘리먼트들을 포함 할 수 있다
<split id="split1" next="step4">
<flow>
<step id="step1" parent="s1" next="step2"/>
<step id="step2" parent="s2"/>
</flow>
<flow>
<step id="step3" parent="s3"/>
</flow>
</split>
<step id="step4" parent="s4"/>
Job의 flow중 일부는 별도의 bean 설정으로 분리 될 수 있으며, 재사용이 가능하다. 이러한 설정 방법에는 3가지가 있으면 그중 첫 번째는 아래와 같이 별도로 설정된 flow를 참조하는 방법이다.
<job id="job">
<flow id="job1.flow1" parent="flow1" next="step3"/>
<step id="step3" parent="s3"/>
</job>
<flow id="flow1">
<step id="step1" parent="s1" next="step2"/>
<step id="step2" parent="s2"/>
</flow>
위와 같이 flow를 설정하게 되면 간단하게 별도의 Step을 추가 할 수 있다. 이런 방법으로 여러 Job에서 같은 flow 템플릿을 참조 할 수 있으며, flow 템플릿의 조합으로 다른 로직의 flow 설정을 할 수 있다. 위 방법은 통합 테스트를 별도의 flow로 분리하는 데에도 유용하게 사용 될 수 있다.
두번째 flow의 분리 방법은 FlowStep을 사용하는 방법이다. FlowStep은 flow의 처리를 <flow/> 엘리먼트로 위임하는 Step 인터페이스의 구현체이다. 아래의 FlowStep 설정 예제를 보자.
<job id="job">
<step id="job1.flow1" flow="flow1" next="step3"/>
<step id="step3" parent="s3"/>
</job>
<flow id="flow1">
<step id="step1" parent="s1" next="step2"/>
<step id="step2" parent="s2"/>
</flow>
실제 로직의 수행은 이전 예제와 동일하나, Job Repository에 저장되는 데이터가 다르게 된다. 이러한 설정 방법은 모니터링 과 리포팅 목적으로 유용하게 사용될 수 있으며 partitioned step의 구조를 더욱 다양하게 해줄 수 있다.
세번째 flow의 분리 방법은 JobStep을 사용하는 방법이다. JobStep은 FlowStep과 유사하지만 분리된 Step의 실행을 위해 별도의 Job Execution을 생성하게 된다.
<job id="jobStepJob" restartable="true">
<step id="jobStepJob.step1">
<job ref="job" job-launcher="jobLauncher"
job-parameters-extractor="jobParametersExtractor"/>
</step>
</job>
<job id="job" restartable="true">...</job>
<bean id="jobParametersExtractor" class="org.spr...DefaultJobParametersExtractor">
<property name="keys" value="input.file"/>
</bean>
위 예제를 보자. job parameters extractor 설정에서 Job이 실행 될때 Step의 ExecutionContext에서 JobParameters를 얻는 방법에 대해 정의한다. JobStep은 Job과 Step의 모니터링을 할때 좀더 세밀한 옵션을 주고 싶을 때 유용하다. JobStep은 “Job 사이의 dependencies를 어떻게 생성하느냐?“의 질문에 대한 좋은 답이 될 수 있다. 위와 같은 설정은 큰 시스템을 작은 모듈로 분리하고 Job의 flow를 컨트롤하는데 유용하게 사용될 수 있다.
Flow Control 활용한 건너뛰기(Skip) 기능 예제